Release 2.25 (#5109)

This commit is contained in:
Kevin Papst
2024-11-21 22:44:49 +01:00
committed by GitHub
parent 49eb7068c9
commit 0c26a2678e
261 changed files with 6431 additions and 7426 deletions

View File

@@ -15,7 +15,7 @@ export default class KimaiMultiUpdateTable extends KimaiPlugin {
init() init()
{ {
if (document.getElementById('multi_update_all') === null) { if (document.getElementsByClassName('multi_update_all').length === 0) {
return; return;
} }
@@ -23,64 +23,64 @@ export default class KimaiMultiUpdateTable extends KimaiPlugin {
// via KimaiDatable and everything inside will be removed, including event listeners // via KimaiDatable and everything inside will be removed, including event listeners
const element = document.querySelector('div.page-body'); const element = document.querySelector('div.page-body');
element.addEventListener('change', (event) => { element.addEventListener('change', (event) => {
if (event.target.matches('#multi_update_all')) { if (event.target.matches('.multi_update_all')) {
// the "check all" checkbox in the upper start corner of the table // the "check all" checkbox in the upper start corner of the table
const checked = event.target.checked; const checked = event.target.checked;
for (const element of document.querySelectorAll('.multi_update_single')) { const table = event.target.closest('table');
for (const element of table.querySelectorAll('.multi_update_single')) {
element.checked = checked; element.checked = checked;
} }
this._toggleForm(); this._toggleForm(table);
event.stopPropagation(); event.stopPropagation();
} else if (event.target.matches('.multi_update_single')) { } else if (event.target.matches('.multi_update_single')) {
// single checkboxes in front of each row // single checkboxes in front of each row
this._toggleForm(); this._toggleForm(event.target.closest('table'));
event.stopPropagation(); event.stopPropagation();
} }
}); });
element.addEventListener('click', (event) => { element.addEventListener('click', (event) => {
if (event.target.matches('.multi_update_table_action')) { if (event.target.matches('.multi_update_table_action')) {
const selectedItem = event.target; const selectedButton = event.target;
const ids = this._getSelectedIds(); const form = selectedButton.form;
const form = document.getElementById('multi_update_form'); const ids = form.querySelector('.multi_update_ids').value.split(',');
const question = form.dataset['question'].replace(/%action%/, selectedItem.textContent).replace(/%count%/, ids.length.toString()); const question = form.dataset['question'].replace(/%action%/, selectedButton.textContent).replace(/%count%/, ids.length.toString());
/** @type {KimaiAlert} ALERT */ /** @type {KimaiAlert} ALERT */
const ALERT = this.getPlugin('alert'); const ALERT = this.getPlugin('alert');
ALERT.question(question, function(value) { ALERT.question(question, function(value) {
if (value) { if (value) {
const form = document.getElementById('multi_update_form'); form.action = selectedButton.dataset['href'];
form.action = selectedItem.dataset['href'];
form.submit(); form.submit();
} }
}); });
} }
}); });
} }
_getSelectedIds() /**
* @param {HTMLTableElement} table
* @private
*/
_toggleForm(table)
{ {
const card = table.closest('div.card.data_table')
let ids = []; let ids = [];
for (const box of document.querySelectorAll('input.multi_update_single:checked')) { for (const box of table.querySelectorAll('input.multi_update_single:checked')) {
ids.push(box.value); ids.push(box.value);
} }
return ids; card.querySelector('.multi_update_ids').value = ids.join(',');
}
_toggleForm()
{
const ids = this._getSelectedIds();
document.getElementById('multi_update_table_entities').value = ids.join(',');
if (ids.length > 0) { if (ids.length > 0) {
for (const element of document.getElementsByClassName('multi_update_form_hide')) { for (const element of card.querySelectorAll('.multi_update_form_hide')) {
element.style.setProperty('display', 'none', 'important'); element.style.setProperty('display', 'none', 'important');
} }
document.getElementById('multi_update_form').style.display = null;//'block'; card.querySelector('form.multi_update_form').style.display = null;//'block';
} else { } else {
document.getElementById('multi_update_form').style.setProperty('display', 'none', 'important'); card.querySelector('form.multi_update_form').style.setProperty('display', 'none', 'important');
for (const element of document.getElementsByClassName('multi_update_form_hide')) { for (const element of card.querySelectorAll('.multi_update_form_hide')) {
element.style.display = null; element.style.display = null;
} }
} }

View File

@@ -5,111 +5,30 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
body { body { font-family: sans-serif; font-size: 10pt; margin: 4px; padding: 0; }
font-family: sans-serif; table, tr, td, th, p, h1, h2, h3, h4, h5 { padding: 0; margin: 0;}
font-size: 10pt; th { border: none; text-align: left; }
margin: 4px; td { vertical-align: top; }
padding: 0; p { margin-bottom: 12px; }
} .text-small { font-size: 7pt; }
table, tr, td, th, p, h1, h2, h3, h4, h5 { .padding-left { padding-left: 15px; }
padding: 0; .text-center { text-align: center; }
margin: 0; .text-left, .text-begin { text-align: left; }
} .text-right, .text-end { text-align: right; }
th { .text-nowrap { white-space: nowrap; }
border: none; .date { font-size: 80%; text-align: right; padding-top: 6px; }
text-align: left; table { width: 100%; border-spacing: 0; }
} table.addresses, p { line-height: 14pt; }
td { table.addresses { margin-bottom: 30px; }
vertical-align: top; table.header { padding-bottom: 4px; border-bottom: 1px solid #999; }
} table.header .title { font-weight: normal; font-size: 140%; }
p { table.footer { border-top: 1px solid #999; font-size: 80%; padding-top: 8px; line-height: 14px; }
margin-bottom: 12px; table.footer td { vertical-align: top; }
} table.items { border-collapse: collapse; margin-top: 50px; margin-bottom: 60px; }
.text-small { table.items thead th, table.items tbody td { border-bottom: 1px solid #ccc; }
font-size: 7pt; table.items thead th.first, table.items tbody td.first { padding-left: 0; }
} table.items thead th.last, table.items tbody td.last, table.items tfoot td.last { padding-right: 0; }
.padding-left { table.items td, table.items th { padding: 8px 6px; }
padding-left: 15px; table.items tfoot td, table.items tfoot th { padding: 20px 0 0 0; line-height: 1px; }
} table.items tfoot td.last { padding-left: 10px; }
.text-center { table.items tr.odd { background-color: #f5f5f5; }
text-align: center;
}
.text-left, .text-begin {
text-align: left;
}
.text-right, .text-end {
text-align: right;
}
.text-nowrap {
white-space: nowrap;
}
.date {
font-size: 80%;
text-align: right;
padding-top: 6px;
}
table {
width: 100%;
border-spacing: 0;
}
table.addresses, p {
line-height: 14pt;
}
/* The address table */
table.addresses {
margin-bottom: 30px;
}
/* The PDF header */
table.header {
padding-bottom: 4px;
border-bottom: 1px solid #999;
}
table.header .title {
font-weight: normal;
font-size: 140%;
}
/* The PDF footer */
table.footer {
border-top: 1px solid #999;
font-size: 80%;
padding-top: 8px;
line-height: 14px;
}
table.footer td {
vertical-align: top;
}
/* The invoice items list */
table.items {
border-collapse: collapse;
margin-top: 50px;
margin-bottom: 60px;
}
table.items thead th,
table.items tbody td {
border-bottom: 1px solid #ccc;
}
table.items thead th.first,
table.items tbody td.first {
padding-left: 0;
}
table.items thead th.last,
table.items tbody td.last,
table.items tfoot td.last {
padding-right: 0;
}
table.items td,
table.items th {
padding: 8px 6px;
}
table.items tfoot td,
table.items tfoot th {
padding: 20px 0 0 0;
line-height: 1px;
}
table.items tfoot td.last {
padding-left: 10px;
}
table.items tr.odd {
background-color: #f5f5f5;
}

View File

@@ -49,3 +49,14 @@ fieldset:empty {
--litepicker-is-in-range-color: var(--tblr-primary-text-emphasis); --litepicker-is-in-range-color: var(--tblr-primary-text-emphasis);
} }
} }
/* fixes contrast of batch-update checkboxes - https://github.com/kimai/kimai/issues/5146 */
.multiupdater[type=checkbox]
{
--tblr-border-color-translucent: rgba(4, 32, 69, .2);
}
/* fixes height for long tags https://github.com/kimai/kimai/issues/5169 */
.tag {
--tblr-tag-height: unset;
}

View File

@@ -98,7 +98,7 @@ table.dataTable {
width: 15px; width: 15px;
} }
&.overlapping { &.overlapping {
border-top: 2px solid rgba(214,57,57,.1); border-top: 2px solid rgba(214,57,57,.2);
} }
&.exported { &.exported {
opacity: 0.7; opacity: 0.7;

View File

@@ -61,6 +61,7 @@
"symfony/mailer": "^6.0", "symfony/mailer": "^6.0",
"symfony/mime": "^6.0", "symfony/mime": "^6.0",
"symfony/monolog-bundle": "^3.4", "symfony/monolog-bundle": "^3.4",
"symfony/process": "^6.0",
"symfony/rate-limiter": "^6.0", "symfony/rate-limiter": "^6.0",
"symfony/runtime": "^6.0", "symfony/runtime": "^6.0",
"symfony/security-bundle": "^6.0", "symfony/security-bundle": "^6.0",
@@ -83,12 +84,12 @@
"doctrine/doctrine-fixtures-bundle": "^3.2", "doctrine/doctrine-fixtures-bundle": "^3.2",
"fakerphp/faker": "^1.15", "fakerphp/faker": "^1.15",
"friendsofphp/php-cs-fixer": "^3.3", "friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.0", "phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^1.0", "phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-symfony": "^1.0", "phpstan/phpstan-symfony": "^2.0",
"phpunit/phpunit": "9.5.*", "phpunit/phpunit": "9.5.*",
"symfony/browser-kit": "^6.0", "symfony/browser-kit": "^6.0",
"symfony/css-selector": "^6.0", "symfony/css-selector": "^6.0",
@@ -134,6 +135,10 @@
"symfony/polyfill-ctype": "*", "symfony/polyfill-ctype": "*",
"symfony/polyfill-mbstring": "*", "symfony/polyfill-mbstring": "*",
"symfony/polyfill-intl": "*", "symfony/polyfill-intl": "*",
"symfony/polyfill-intl-icu": "*",
"symfony/polyfill-intl-grapheme": "*",
"symfony/polyfill-intl-idn": "*",
"symfony/polyfill-intl-normalizer": "*",
"symfony/polyfill-iconv": "*", "symfony/polyfill-iconv": "*",
"symfony/polyfill-php81": "*", "symfony/polyfill-php81": "*",
"symfony/polyfill-php80": "*", "symfony/polyfill-php80": "*",

1698
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,9 +24,9 @@
], ],
"packageManager": "yarn@3.2.1", "packageManager": "yarn@3.2.1",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/eslint-parser": "^7.19", "@babel/eslint-parser": "^7.19",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@fortawesome/fontawesome-free": "^6.5", "@fortawesome/fontawesome-free": "^6.5",
"@fullcalendar/bootstrap5": "^5.11", "@fullcalendar/bootstrap5": "^5.11",
"@fullcalendar/core": "^5.11", "@fullcalendar/core": "^5.11",
@@ -47,7 +47,10 @@
"sass": "^1.62", "sass": "^1.62",
"sass-loader": "^13.2", "sass-loader": "^13.2",
"tom-select": "^2.2", "tom-select": "^2.2",
"webpack": "^5.91", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^4.10"
},
"dependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.8.3"
} }
} }

View File

@@ -21,12 +21,10 @@ parameters:
booleansInConditions: false booleansInConditions: false
uselessCast: true uselessCast: true
requireParentConstructorCall: true requireParentConstructorCall: true
disallowedConstructs: false
overwriteVariablesWithLoop: true overwriteVariablesWithLoop: true
closureUsesThis: true closureUsesThis: true
matchingInheritedMethodNames: true matchingInheritedMethodNames: true
numericOperandsInArithmeticOperators: true numericOperandsInArithmeticOperators: true
strictCalls: false
switchConditionsMatchingType: true switchConditionsMatchingType: true
noVariableVariables: false noVariableVariables: false
paths: paths:
@@ -290,7 +288,7 @@ parameters:
path: src/Command/ExportCreateCommand.php path: src/Command/ExportCreateCommand.php
- -
message: "#^Parameter \\#1 \\$locale of static method Symfony\\\\Polyfill\\\\Intl\\\\Icu\\\\Locale\\:\\:setDefault\\(\\) expects string, mixed given\\.$#" message: "#^Parameter \\#1 \\$locale of static method Locale\\:\\:setDefault\\(\\) expects string, mixed given\\.$#"
count: 1 count: 1
path: src/Command/ExportCreateCommand.php path: src/Command/ExportCreateCommand.php
@@ -315,7 +313,7 @@ parameters:
path: src/Command/ExportCreateCommand.php path: src/Command/ExportCreateCommand.php
- -
message: "#^Parameter \\#2 \\$name of method Symfony\\\\Component\\\\HttpFoundation\\\\File\\\\File\\:\\:move\\(\\) expects string\\|null, array\\<int, string\\>\\|string given\\.$#" message: "#^Parameter \\#2 \\$name of method Symfony\\\\Component\\\\HttpFoundation\\\\File\\\\File\\:\\:move\\(\\) expects string\\|null, list\\<string\\>\\|string given\\.$#"
count: 1 count: 1
path: src/Command/ExportCreateCommand.php path: src/Command/ExportCreateCommand.php
@@ -465,7 +463,7 @@ parameters:
path: src/Command/TranslationCommand.php path: src/Command/TranslationCommand.php
- -
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#" message: "#^Argument of an invalid type list\\<string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 4 count: 4
path: src/Command/TranslationCommand.php path: src/Command/TranslationCommand.php
@@ -1294,16 +1292,6 @@ parameters:
count: 1 count: 1
path: src/Entity/Team.php path: src/Entity/Team.php
-
message: "#^Method App\\\\Entity\\\\Team\\:\\:getTeamleads\\(\\) should return array\\<App\\\\Entity\\\\User\\> but returns array\\<int, App\\\\Entity\\\\User\\|null\\>\\.$#"
count: 1
path: src/Entity/Team.php
-
message: "#^Method App\\\\Entity\\\\Team\\:\\:getUsers\\(\\) should return array\\<App\\\\Entity\\\\User\\> but returns array\\<int, App\\\\Entity\\\\User\\|null\\>\\.$#"
count: 1
path: src/Entity/Team.php
- -
message: "#^Property App\\\\Entity\\\\Team\\:\\:\\$activities with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" message: "#^Property App\\\\Entity\\\\Team\\:\\:\\$activities with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
count: 1 count: 1
@@ -1465,7 +1453,7 @@ parameters:
path: src/Entity/UserPreference.php path: src/Entity/UserPreference.php
- -
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, string\\|null given\\.$#"
count: 1 count: 1
path: src/Entity/UserPreference.php path: src/Entity/UserPreference.php
@@ -1479,11 +1467,6 @@ parameters:
count: 1 count: 1
path: src/Entity/UserPreference.php path: src/Entity/UserPreference.php
-
message: "#^Property App\\\\Entity\\\\UserPreference\\:\\:\\$value \\(string\\|null\\) does not accept mixed\\.$#"
count: 1
path: src/Entity/UserPreference.php
- -
message: "#^Method App\\\\Event\\\\AbstractTimesheetMultipleEvent\\:\\:getTimesheets\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Event\\\\AbstractTimesheetMultipleEvent\\:\\:getTimesheets\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -1809,11 +1792,6 @@ parameters:
count: 1 count: 1
path: src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php path: src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php
-
message: "#^Method App\\\\Export\\\\Spreadsheet\\\\Extractor\\\\AnnotationExtractor\\:\\:extract\\(\\) should return array\\<App\\\\Export\\\\Spreadsheet\\\\ColumnDefinition\\> but returns array\\<int, App\\\\Export\\\\Spreadsheet\\\\ColumnDefinition\\|null\\>\\.$#"
count: 1
path: src/Export/Spreadsheet/Extractor/AnnotationExtractor.php
- -
message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<T of object\\>\\|T of object, string given\\.$#" message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<T of object\\>\\|T of object, string given\\.$#"
count: 1 count: 1
@@ -2005,7 +1983,7 @@ parameters:
path: src/Form/Helper/ActivityHelper.php path: src/Form/Helper/ActivityHelper.php
- -
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#" message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1 count: 1
path: src/Form/Helper/ActivityHelper.php path: src/Form/Helper/ActivityHelper.php
@@ -2020,7 +1998,7 @@ parameters:
path: src/Form/Helper/CustomerHelper.php path: src/Form/Helper/CustomerHelper.php
- -
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#" message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1 count: 1
path: src/Form/Helper/CustomerHelper.php path: src/Form/Helper/CustomerHelper.php
@@ -2035,7 +2013,7 @@ parameters:
path: src/Form/Helper/ProjectHelper.php path: src/Form/Helper/ProjectHelper.php
- -
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#" message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1 count: 1
path: src/Form/Helper/ProjectHelper.php path: src/Form/Helper/ProjectHelper.php
@@ -3365,7 +3343,7 @@ parameters:
path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
- -
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, mixed given\\.$#" message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, mixed given\\.$#"
count: 1 count: 1
path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
@@ -3395,7 +3373,7 @@ parameters:
path: src/Invoice/Renderer/DocxRenderer.php path: src/Invoice/Renderer/DocxRenderer.php
- -
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, mixed given\\.$#" message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\<float\\|int\\|string\\>\\|string, mixed given\\.$#"
count: 2 count: 2
path: src/Invoice/Renderer/DocxRenderer.php path: src/Invoice/Renderer/DocxRenderer.php
@@ -3559,11 +3537,6 @@ parameters:
count: 1 count: 1
path: src/Ldap/LdapUserProvider.php path: src/Ldap/LdapUserProvider.php
-
message: "#^Parameter \\#1 \\.\\.\\.\\$addresses of method Symfony\\\\Component\\\\Mime\\\\Email\\:\\:from\\(\\) expects string\\|Symfony\\\\Component\\\\Mime\\\\Address, string\\|null given\\.$#"
count: 1
path: src/Mail/KimaiMailer.php
- -
message: "#^Parameter \\#1 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#" message: "#^Parameter \\#1 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#"
count: 1 count: 1
@@ -3579,11 +3552,6 @@ parameters:
count: 1 count: 1
path: src/Model/MonthlyStatistic.php path: src/Model/MonthlyStatistic.php
-
message: "#^Method App\\\\Model\\\\MonthlyStatistic\\:\\:getYears\\(\\) should return array\\<string\\> but returns array\\<int, int\\|string\\>\\.$#"
count: 1
path: src/Model/MonthlyStatistic.php
- -
message: "#^Method App\\\\Model\\\\Statistic\\\\Day\\:\\:getDetails\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Model\\\\Statistic\\\\Day\\:\\:getDetails\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -4450,7 +4418,7 @@ parameters:
path: src/Utils/LocaleFormatter.php path: src/Utils/LocaleFormatter.php
- -
message: "#^Method App\\\\Utils\\\\LocaleFormatter\\:\\:durationDecimal\\(\\) should return string but returns bool\\|string\\.$#" message: "#^Method App\\\\Utils\\\\LocaleFormatter\\:\\:durationDecimal\\(\\) should return string but returns string\\|false\\.$#"
count: 1 count: 1
path: src/Utils/LocaleFormatter.php path: src/Utils/LocaleFormatter.php
@@ -4460,17 +4428,17 @@ parameters:
path: src/Utils/LocaleFormatter.php path: src/Utils/LocaleFormatter.php
- -
message: "#^Method App\\\\Utils\\\\LocaleFormatter\\:\\:money\\(\\) should return string but returns bool\\|string\\.$#" message: "#^Method App\\\\Utils\\\\LocaleFormatter\\:\\:money\\(\\) should return string but returns string\\|false\\.$#"
count: 2
path: src/Utils/LocaleFormatter.php
-
message: "#^Parameter \\#1 \\$num of method NumberFormatter\\:\\:format\\(\\) expects float\\|int, float\\|int\\|string given\\.$#"
count: 1 count: 1
path: src/Utils/LocaleFormatter.php path: src/Utils/LocaleFormatter.php
- -
message: "#^Parameter \\#1 \\$num of method Symfony\\\\Polyfill\\\\Intl\\\\Icu\\\\NumberFormatter\\:\\:format\\(\\) expects float\\|int, float\\|int\\|string given\\.$#" message: "#^Parameter \\#2 \\$currency of method NumberFormatter\\:\\:formatCurrency\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/Utils/LocaleFormatter.php
-
message: "#^Parameter \\#2 \\$currency of method Symfony\\\\Polyfill\\\\Intl\\\\Icu\\\\NumberFormatter\\:\\:formatCurrency\\(\\) expects string, string\\|null given\\.$#"
count: 1 count: 1
path: src/Utils/LocaleFormatter.php path: src/Utils/LocaleFormatter.php

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@
"app": { "app": {
"js": [ "js": [
"/build/runtime.74179306.js", "/build/runtime.74179306.js",
"/build/app.6cdb49cb.js" "/build/app.06423747.js"
], ],
"css": [ "css": [
"/build/app.1ed18844.css" "/build/app.6bd98062.css"
] ]
}, },
"app-rtl": { "app-rtl": {
@@ -15,7 +15,7 @@
"/build/app-rtl.97153087.js" "/build/app-rtl.97153087.js"
], ],
"css": [ "css": [
"/build/app-rtl.d38ad12c.css" "/build/app-rtl.6a14a027.css"
] ]
}, },
"export-pdf": { "export-pdf": {
@@ -72,10 +72,10 @@
}, },
"integrity": { "integrity": {
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG", "/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
"/build/app.6cdb49cb.js": "sha384-psXCV8SG68O80ZJHHeSbCJPQ8RoepaMeI1pEypTHu0Q8ySLrhlclhKHIL2dZ0ZQJ", "/build/app.06423747.js": "sha384-9bVxz1eOxdhs+XNwJ6Ug4/crO0CrQlWs1FDYQ1xF14LpFIZjuVO65YohHT3KWreU",
"/build/app.1ed18844.css": "sha384-fEjS/tjc8m7jrw28/eWZFOTO6bugR9ILfksKvBrF46wtmqlage1s/P4+ZeYuqgpq", "/build/app.6bd98062.css": "sha384-BB9TrvQK/jlfhbdISEdBH5ZaZvbrPnZ+AcocXs1uQNh2yIQFboslUY9Up3iqnY4h",
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr", "/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
"/build/app-rtl.d38ad12c.css": "sha384-eFvMRueGVAxDxpr3s4y1EhHnVb8FQOr+KQgOuTzbW11ILydpLIvObiiuqNpf4mWO", "/build/app-rtl.6a14a027.css": "sha384-ZmUAtO0Z/WfjYxJNleP3rBJpDmAnbaTlFMtejV/YpsshxkqmyNUneCIos9uMX7kH",
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF", "/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG", "/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",

View File

@@ -1,7 +1,7 @@
{ {
"build/app.css": "/build/app.1ed18844.css", "build/app.css": "/build/app.6bd98062.css",
"build/app.js": "/build/app.6cdb49cb.js", "build/app.js": "/build/app.06423747.js",
"build/app-rtl.css": "/build/app-rtl.d38ad12c.css", "build/app-rtl.css": "/build/app-rtl.6a14a027.css",
"build/app-rtl.js": "/build/app-rtl.97153087.js", "build/app-rtl.js": "/build/app-rtl.97153087.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.1442bee7.js", "build/export-pdf.js": "/build/export-pdf.1442bee7.js",

View File

@@ -4,9 +4,6 @@ use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// TODO remove once PARTIAL usage was replaced entirely
\Doctrine\Deprecations\Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/8471');
return function (array $context) { return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
}; };

View File

@@ -17,7 +17,7 @@ use App\Entity\Timesheet;
use App\Event\PageActionsEvent; use App\Event\PageActionsEvent;
use FOS\RestBundle\View\View; use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;

View File

@@ -13,7 +13,7 @@ use App\API\Model\TimesheetConfig;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use FOS\RestBundle\View\View; use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;

View File

@@ -14,7 +14,7 @@ use App\API\Model\Version;
use App\Plugin\PluginManager; use App\Plugin\PluginManager;
use FOS\RestBundle\View\View; use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;

View File

@@ -15,11 +15,11 @@ use App\Plugin\Plugin;
use App\Plugin\PluginManager; use App\Plugin\PluginManager;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\PhpSubprocess;
#[AsCommand(name: 'kimai:plugins', description: 'Manage Kimai plugins')] #[AsCommand(name: 'kimai:plugins', description: 'Manage Kimai plugins')]
final class PluginCommand extends Command final class PluginCommand extends Command
@@ -69,14 +69,28 @@ final class PluginCommand extends Command
} }
} }
$command = $this->getApplication()?->find('doctrine:migrations:migrate'); // using getApplication()->find('doctrine:migrations:migrate') does NOT work here
if ($command === null) { // because the Doctrine command can only be executed once
throw new \RuntimeException('Failed finding doctrine migrations command'); // if run more than once it fails with a "Container is frozen" exception
}
$cmdInput = new ArrayInput(['--allow-no-migration' => true, '--configuration' => $config]); $process = new PhpSubprocess([
$cmdInput->setInteractive(false); 'bin/console',
if (0 !== $command->run($cmdInput, $output)) { 'doctrine:migrations:migrate',
$io->error('Failed to install bundle database: ' . $config); '--allow-no-migration',
'--no-interaction',
'--configuration=' . $config
]);
$process->run();
if (!$process->isSuccessful()) {
$io->error('Failed to install bundle database: ' . PHP_EOL . $config);
$io->error($process->getErrorOutput());
} else {
if ($io->isVerbose()) {
$io->write($process->getOutput());
} else {
$io->success('Successfully installed: ' . $plugin->getName());
}
} }
} }

View File

@@ -136,7 +136,15 @@ final class RegenerateLocalesCommand extends Command
$shortTime = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT); $shortTime = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT);
$settings['date'] = $shortDate->getPattern(); $settings['date'] = $shortDate->getPattern();
if ($settings['date'] === false) {
$io->error('Invalid date pattern for locale: ' . $locale);
continue;
}
$settings['time'] = $shortTime->getPattern(); $settings['time'] = $shortTime->getPattern();
if ($settings['time'] === false) {
$io->error('Invalid time pattern for locale: ' . $locale);
continue;
}
// see https://github.com/kimai/kimai/issues/4402 - Korean time format failed parsing // see https://github.com/kimai/kimai/issues/4402 - Korean time format failed parsing
// special case when time pattern starts with A / a => this will lead to an error // special case when time pattern starts with A / a => this will lead to an error

View File

@@ -48,7 +48,9 @@ final class TranslationCommand extends Command
->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version') ->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version')
->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all') ->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all')
->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)') ->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)')
->addOption('move-all', null, InputOption::VALUE_NONE, 'Move all keys from one file to another (needs "source" and "target" options)')
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use') ->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use')
->addOption('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
->addOption('target', null, InputOption::VALUE_REQUIRED, 'Single target file to use') ->addOption('target', null, InputOption::VALUE_REQUIRED, 'Single target file to use')
// DEEPL TRANSLATION FEATURE - UNTESTED // DEEPL TRANSLATION FEATURE - UNTESTED
->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale with Deepl') ->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale with Deepl')
@@ -68,10 +70,13 @@ final class TranslationCommand extends Command
$bases = [ $bases = [
'core' => $this->projectDirectory . '/translations/*.xlf', 'core' => $this->projectDirectory . '/translations/*.xlf',
'plugins' => $this->projectDirectory . Kernel::PLUGIN_DIRECTORY . '/*/Resources/translations/*.xlf',
'theme' => $this->projectDirectory . '/vendor/kevinpapst/tabler-bundle/translations/*.xlf',
]; ];
if (!$input->getOption('only-core')) {
$bases['plugins'] = $this->projectDirectory . Kernel::PLUGIN_DIRECTORY . '/*/Resources/translations/*.xlf';
$bases['theme'] = $this->projectDirectory . '/vendor/kevinpapst/tabler-bundle/translations/*.xlf';
}
$sources = []; $sources = [];
if ($input->getOption('source') !== null) { if ($input->getOption('source') !== null) {
/** @var string $tmp */ /** @var string $tmp */
@@ -146,6 +151,19 @@ final class TranslationCommand extends Command
return $this->moveResname($io, $moveResname, $sources, $targets); return $this->moveResname($io, $moveResname, $sources, $targets);
} }
// ==========================================================================
// Move all keys from source to target
// ==========================================================================
if ($input->getOption('move-all')) {
if (\count($sources) === 0 || \count($targets) === 0) {
$io->error('Moving all keys only works with one source and one target file');
return Command::FAILURE;
}
return $this->moveAll($io, $sources, $targets);
}
// ========================================================================== // ==========================================================================
// Fill empty translations with english version // Fill empty translations with english version
// ========================================================================== // ==========================================================================
@@ -634,4 +652,66 @@ final class TranslationCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
/**
* @param array<string> $sources
* @param array<string> $targets
*/
private function moveAll(SymfonyStyle $io, array $sources, array $targets): int
{
foreach ($sources as $source) {
$tmp = basename($source);
$pos = strpos($tmp, '.');
if ($pos === false) {
$io->error('Unexpected filename: ' . $source);
return Command::FAILURE;
}
$suffix = substr($tmp, $pos);
$target = null;
foreach ($targets as $t) {
if (str_ends_with($t, $suffix)) {
$target = $t;
}
}
if ($target === null) {
$io->error('Cannot find translation target file for source: ' . $source);
return Command::FAILURE;
}
$sourceDocument = new \DOMDocument('1.0');
$sourceDocument->load($source);
$targetDocument = new \DOMDocument('1.0');
$targetDocument->load($target);
$removeNodes = [];
/** @var \DOMElement $element */
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
$newNode = $targetDocument->importNode($element, true);
$targetDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
$removeNodes[] = $element;
}
foreach ($removeNodes as $node) {
$sourceDocument->documentElement->firstElementChild->firstElementChild->removeChild($node); // @phpstan-ignore-line
}
$xmlDocument = new \DOMDocument('1.0');
$xmlDocument->preserveWhiteSpace = false;
$xmlDocument->formatOutput = true;
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
file_put_contents($source, $xmlDocument->saveXML());
$xmlDocument = new \DOMDocument('1.0');
$xmlDocument->preserveWhiteSpace = false;
$xmlDocument->formatOutput = true;
$xmlDocument->loadXML($targetDocument->saveXML()); // @phpstan-ignore-line
file_put_contents($target, $xmlDocument->saveXML());
}
return Command::SUCCESS;
}
} }

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.24.0'; public const VERSION = '2.25.0';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 22400; public const VERSION_ID = 22500;
/** /**
* The software name * The software name
*/ */

View File

@@ -111,6 +111,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
$tags[] = $tag; $tags[] = $tag;
} }
$newTimesheets = [];
foreach ($allUsers as $user) { foreach ($allUsers as $user) {
$newTimesheet = $entry->createCopy(); $newTimesheet = $entry->createCopy();
$newTimesheet->setUser($user); $newTimesheet->setUser($user);
@@ -118,6 +119,11 @@ final class TimesheetTeamController extends TimesheetAbstractController
$newTimesheet->addTag($tag); $newTimesheet->addTag($tag);
} }
$this->service->prepareNewTimesheet($newTimesheet, $request); $this->service->prepareNewTimesheet($newTimesheet, $request);
$this->service->validateTimesheet($newTimesheet);
$newTimesheets[] = $newTimesheet;
}
foreach ($newTimesheets as $newTimesheet) {
$this->service->saveNewTimesheet($newTimesheet); $this->service->saveNewTimesheet($newTimesheet);
} }

View File

@@ -24,7 +24,7 @@ final class UTCDateTimeImmutableType extends DateTimeImmutableType
/** /**
* @param T $value * @param T $value
* @return (T is null ? null : string) * @return (T is null ? null : string)
* @template T<\DateTimeImmutable> * @template T
* @throws ConversionException * @throws ConversionException
*/ */
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string

View File

@@ -25,7 +25,7 @@ final class UTCDateTimeType extends DateTimeType
* @param T $value * @param T $value
* @param AbstractPlatform $platform * @param AbstractPlatform $platform
* @return (T is null ? null : string) * @return (T is null ? null : string)
* @template T<\DateTime> * @template T
* @throws ConversionException * @throws ConversionException
*/ */
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string

View File

@@ -109,16 +109,14 @@ trait MetaTableTypeTrait
// unchecked checkboxes / false bool would save an empty string in the database // unchecked checkboxes / false bool would save an empty string in the database
// those cannot be searched in the database // those cannot be searched in the database
if (null !== $value) { switch ($this->type) {
switch ($this->type) { case YesNoType::class:
case YesNoType::class: case CheckboxType::class:
case CheckboxType::class: if ($value === false || $value === '' || !\is_scalar($value)) {
if (!\is_int($value) && !\is_bool($value) && !\is_string($value)) { $value = 0;
throw new \InvalidArgumentException('Failed converting meta-field bool value'); } else {
} else { $value = 1;
$value = (string) $value; }
}
}
} }
if ($value === null) { if ($value === null) {

View File

@@ -178,13 +178,13 @@ class Team
} }
/** /**
* @return User[] * @return list<User>
*/ */
public function getTeamleads(): array public function getTeamleads(): array
{ {
$leads = []; $leads = [];
foreach ($this->members as $member) { foreach ($this->members as $member) {
if ($member->isTeamlead()) { if ($member->isTeamlead() && $member->getUser() !== null) {
$leads[] = $member->getUser(); $leads[] = $member->getUser();
} }
} }
@@ -273,13 +273,15 @@ class Team
/** /**
* Returns all users in the team, both teamlead and normal member. * Returns all users in the team, both teamlead and normal member.
* *
* @return User[] * @return list<User>
*/ */
public function getUsers(): array public function getUsers(): array
{ {
$users = []; $users = [];
foreach ($this->members as $member) { foreach ($this->members as $member) {
$users[] = $member->getUser(); if ($member->getUser() !== null) {
$users[] = $member->getUser();
}
} }
return $users; return $users;

View File

@@ -624,11 +624,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
return $this; return $this;
} }
public function createCopy(?Timesheet $timesheet = null): Timesheet public function createCopy(): Timesheet
{ {
if (null === $timesheet) { $timesheet = new Timesheet();
$timesheet = new Timesheet();
}
$values = get_object_vars($this); $values = get_object_vars($this);
foreach ($values as $k => $v) { foreach ($values as $k => $v) {
@@ -639,7 +637,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/** @var TimesheetMeta $meta */ /** @var TimesheetMeta $meta */
foreach ($this->meta as $meta) { foreach ($this->meta as $meta) {
$timesheet->setMetaField(clone $meta); $tmp = clone $meta;
$tmp->setEntity($timesheet);
$timesheet->setMetaField($tmp);
} }
$timesheet->tags = new ArrayCollection(); $timesheet->tags = new ArrayCollection();

View File

@@ -752,9 +752,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
* This method should not be called by plugins and returns true on success or false on a failure. * This method should not be called by plugins and returns true on success or false on a failure.
* *
* @internal immutable property that cannot be set by plugins * @internal immutable property that cannot be set by plugins
* @param bool $canSeeAllData
* @return bool
* @throws Exception
*/ */
public function initCanSeeAllData(bool $canSeeAllData): bool public function initCanSeeAllData(bool $canSeeAllData): bool
{ {

View File

@@ -150,11 +150,26 @@ class UserPreference
* integer, float, string, boolean or null * integer, float, string, boolean or null
* *
* @param mixed $value * @param mixed $value
* @return UserPreference
*/ */
public function setValue($value): UserPreference public function setValue($value): UserPreference
{ {
$this->value = $value; // unchecked checkboxes / false bool would save an empty string in the database
// those cannot be searched in the database
switch ($this->type) {
case YesNoType::class:
case CheckboxType::class:
if ($value === false || $value === '' || !\is_scalar($value)) {
$value = 0;
} else {
$value = 1;
}
}
if ($value === null) {
$this->value = $value;
} elseif (\is_scalar($value)) {
$this->value = (string) $value;
}
return $this; return $this;
} }

View File

@@ -157,7 +157,7 @@ class PageActionsEvent extends ThemeEvent
public function addQuickImport(string $url): void public function addQuickImport(string $url): void
{ {
$this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload', 'translation_domain' => 'actions']); $this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload']);
} }
public function addQuickExport(string $url): void public function addQuickExport(string $url): void
@@ -172,7 +172,7 @@ class PageActionsEvent extends ThemeEvent
public function addEdit(string $url, bool $modal = true, string $class = ''): void public function addEdit(string $url, bool $modal = true, string $class = ''): void
{ {
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' . ($class === '' ? '' : ' ' . $class) : $class), 'translation_domain' => 'actions', 'title' => 'edit']); $this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' . ($class === '' ? '' : ' ' . $class) : $class), 'title' => 'edit']);
} }
/** /**
@@ -180,20 +180,20 @@ class PageActionsEvent extends ThemeEvent
*/ */
public function addSettings(string $url): void public function addSettings(string $url): void
{ {
$this->addAction('settings', ['url' => $url, 'class' => 'modal-ajax-form', 'title' => 'settings', 'translation_domain' => 'actions', 'accesskey' => 'h']); $this->addAction('settings', ['url' => $url, 'class' => 'modal-ajax-form', 'title' => 'settings', 'accesskey' => 'h']);
} }
public function addConfig(string $url): void public function addConfig(string $url): void
{ {
$this->addAction('settings', ['url' => $url, 'title' => 'settings', 'translation_domain' => 'actions']); $this->addAction('settings', ['url' => $url, 'title' => 'settings']);
} }
public function addDelete(string $url, bool $remoteConfirm = true): void public function addDelete(string $url, bool $remoteConfirm = true): void
{ {
if ($remoteConfirm) { if ($remoteConfirm) {
$this->addAction('trash', ['url' => $url, 'class' => 'modal-ajax-form text-red', 'translation_domain' => 'actions', 'title' => 'trash']); $this->addAction('trash', ['url' => $url, 'class' => 'modal-ajax-form text-red', 'title' => 'trash']);
} else { } else {
$this->addAction('trash', ['url' => $url, 'class' => 'confirmation-link text-red', 'attr' => ['data-question' => 'confirm.delete'], 'translation_domain' => 'actions', 'title' => 'trash']); $this->addAction('trash', ['url' => $url, 'class' => 'confirmation-link text-red', 'attr' => ['data-question' => 'confirm.delete'], 'title' => 'trash']);
} }
} }

View File

@@ -21,7 +21,7 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
{ {
private ?string $locale = null; private ?string $locale = null;
public function __construct(private AuthorizationCheckerInterface $auth, private UrlGeneratorInterface $urlGenerator) public function __construct(private readonly AuthorizationCheckerInterface $auth, private readonly UrlGeneratorInterface $urlGenerator)
{ {
} }

View File

@@ -29,7 +29,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
} }
if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) { if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) {
$event->addAction('repeat', ['title' => 'repeat', 'translation_domain' => 'actions', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]); $event->addAction('repeat', ['title' => 'repeat', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
} }
if ($this->isGranted('edit', $timesheet)) { if ($this->isGranted('edit', $timesheet)) {
@@ -38,7 +38,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('duplicate', $timesheet)) { if ($this->isGranted('duplicate', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form'; $class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('copy', ['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class . ' dd-ts-duplicate']); $event->addAction('copy', ['title' => 'copy', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class . ' dd-ts-duplicate']);
} }
if ($event->countActions() > 0) { if ($event->countActions() > 0) {
@@ -49,7 +49,6 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
$event->addAction('trash', [ $event->addAction('trash', [
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]), 'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
'class' => 'api-link text-red dd-ts-trash', 'class' => 'api-link text-red dd-ts-trash',
'translation_domain' => 'actions',
'attr' => [ 'attr' => [
'data-event' => 'kimai.timesheetDelete', 'data-event' => 'kimai.timesheetDelete',
'data-method' => 'DELETE', 'data-method' => 'DELETE',

View File

@@ -31,7 +31,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
} }
if (!$event->isView('activity_details') && $this->isGranted('view', $activity)) { if (!$event->isView('activity_details') && $this->isGranted('view', $activity)) {
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('activity_details', ['id' => $activity->getId()])]); $event->addAction('details', ['title' => 'details', 'url' => $this->path('activity_details', ['id' => $activity->getId()])]);
} }
if ($this->isGranted('edit', $activity)) { if ($this->isGranted('edit', $activity)) {
@@ -40,7 +40,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $activity)) { if ($this->isGranted('permissions', $activity)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form'; $class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]); $event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]);
} }
if ($event->countActions() > 0) { if ($event->countActions() > 0) {
@@ -53,7 +53,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
$parameters['customers[]'] = $activity->getProject()->getCustomer()->getId(); $parameters['customers[]'] = $activity->getProject()->getCustomer()->getId();
$parameters['projects[]'] = $activity->getProject()->getId(); $parameters['projects[]'] = $activity->getProject()->getId();
} }
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', $parameters)]); $event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', $parameters)]);
} }
if ($event->hasSubmenu('filter')) { if ($event->hasSubmenu('filter')) {
@@ -65,7 +65,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
if (!$activity->isGlobal()) { if (!$activity->isGlobal()) {
$parameters['project'] = $activity->getProject()->getId(); $parameters['project'] = $activity->getProject()->getId();
} }
$event->addAction('create-timesheet', ['title' => 'create-timesheet', 'translation_domain' => 'actions', 'icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']); $event->addAction('create-timesheet', ['title' => 'create-timesheet', 'icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
} }
if (($event->isIndexView() || $event->isView('project_details')) && $this->isGranted('delete', $activity)) { if (($event->isIndexView() || $event->isView('project_details')) && $this->isGranted('delete', $activity)) {

View File

@@ -34,7 +34,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
$isListingView = $event->isIndexView() || $event->isCustomView(); $isListingView = $event->isIndexView() || $event->isCustomView();
if (!$event->isView('customer_details') && $canView) { if (!$event->isView('customer_details') && $canView) {
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('customer_details', ['id' => $customer->getId()])]); $event->addAction('details', ['title' => 'details', 'url' => $this->path('customer_details', ['id' => $customer->getId()])]);
} }
if ($this->isGranted('edit', $customer)) { if ($this->isGranted('edit', $customer)) {
@@ -43,7 +43,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $customer)) { if ($this->isGranted('permissions', $customer)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form'; $class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]); $event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
} }
if ($isListingView) { if ($isListingView) {
@@ -61,15 +61,15 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('view_project') || $this->isGranted('view_teamlead_project') || $this->isGranted('view_team_project')) { if ($this->isGranted('view_project') || $this->isGranted('view_teamlead_project') || $this->isGranted('view_team_project')) {
$event->addActionToSubmenu('filter', 'project', ['title' => 'project.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_project', ['customers[]' => $customer->getId()])]); $event->addActionToSubmenu('filter', 'project', ['title' => 'project.filter', 'url' => $this->path('admin_project', ['customers[]' => $customer->getId()])]);
} }
if ($this->isGranted('view_activity')) { if ($this->isGranted('view_activity')) {
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId()])]); $event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId()])]);
} }
if ($this->isGranted('view_other_timesheet')) { if ($this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId()])]); $event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId()])]);
} }
if ($event->hasSubmenu('filter')) { if ($event->hasSubmenu('filter')) {

View File

@@ -56,7 +56,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
$allowDelete = $this->isGranted('delete_invoice'); $allowDelete = $this->isGranted('delete_invoice');
if (!$invoice->isCanceled()) { if (!$invoice->isCanceled()) {
$id = $allowDelete ? 'invoice.cancel' : 'trash'; $id = $allowDelete ? 'invoice.cancel' : 'trash';
$event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled', 'token' => $payload['token']]), 'title' => 'invoice.cancel', 'translation_domain' => 'actions']); $event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled', 'token' => $payload['token']]), 'title' => 'invoice.cancel']);
} }
if ($this->isGranted('delete_invoice')) { if ($this->isGranted('delete_invoice')) {

View File

@@ -34,7 +34,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
$isListingView = $event->isIndexView() || $event->isCustomView(); $isListingView = $event->isIndexView() || $event->isCustomView();
if (!$event->isView('project_details') && $this->isGranted('view', $project)) { if (!$event->isView('project_details') && $this->isGranted('view', $project)) {
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('project_details', ['id' => $project->getId()])]); $event->addAction('details', ['title' => 'details', 'url' => $this->path('project_details', ['id' => $project->getId()])]);
} }
if ($this->isGranted('edit', $project)) { if ($this->isGranted('edit', $project)) {
@@ -43,7 +43,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $project)) { if ($this->isGranted('permissions', $project)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form'; $class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]); $event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]);
} }
if ($event->countActions() > 0) { if ($event->countActions() > 0) {
@@ -51,11 +51,11 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('view_activity')) { if ($this->isGranted('view_activity')) {
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]); $event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
} }
if ($this->isGranted('view_other_timesheet')) { if ($this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]); $event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
} }
if ($this->isGranted('create_export')) { if ($this->isGranted('create_export')) {
@@ -79,7 +79,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
if (\array_key_exists('token', $payload) && $this->isGranted('edit', $project) && $this->isGranted('create_project')) { if (\array_key_exists('token', $payload) && $this->isGranted('edit', $project) && $this->isGranted('create_project')) {
$event->addAction( $event->addAction(
'copy', 'copy',
['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])] ['title' => 'copy', 'url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
); );
} }

View File

@@ -46,14 +46,13 @@ final class TagSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('view_other_timesheet')) { if ($this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['tags' => $name])]); $event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['tags' => $name])]);
} }
if ($event->isIndexView() && $this->isGranted('delete_tag')) { if ($event->isIndexView() && $this->isGranted('delete_tag')) {
$event->addAction('trash', [ $event->addAction('trash', [
'url' => $this->path('delete_tag', ['id' => $id]), 'url' => $this->path('delete_tag', ['id' => $id]),
'class' => 'api-link text-red', 'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [ 'attr' => [
'data-event' => 'kimai.tagDelete kimai.tagUpdate', 'data-event' => 'kimai.tagDelete kimai.tagUpdate',
'data-method' => 'DELETE', 'data-method' => 'DELETE',

View File

@@ -36,7 +36,7 @@ final class TeamSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('create_team')) { if ($this->isGranted('create_team')) {
$event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()]), 'title' => 'copy', 'translation_domain' => 'actions', 'class' => 'modal-ajax-form']); $event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()]), 'title' => 'copy', 'class' => 'modal-ajax-form']);
} }
} }
@@ -44,7 +44,6 @@ final class TeamSubscriber extends AbstractActionsSubscriber
$event->addAction('trash', [ $event->addAction('trash', [
'url' => $this->path('delete_team', ['id' => $team->getId()]), 'url' => $this->path('delete_team', ['id' => $team->getId()]),
'class' => 'api-link text-red', 'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [ 'attr' => [
'data-event' => 'kimai.teamDelete kimai.teamUpdate', 'data-event' => 'kimai.teamDelete kimai.teamUpdate',
'data-method' => 'DELETE', 'data-method' => 'DELETE',

View File

@@ -22,7 +22,7 @@ final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
{ {
if ($this->isGranted('create_other_timesheet')) { if ($this->isGranted('create_other_timesheet')) {
$event->addAction('create', ['title' => 'create', 'url' => $this->path('admin_timesheet_create'), 'class' => 'create-ts modal-ajax-form']); $event->addAction('create', ['title' => 'create', 'url' => $this->path('admin_timesheet_create'), 'class' => 'create-ts modal-ajax-form']);
$event->addAction('multi-user', ['title' => 'create-timesheet-multiuser', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form', 'icon' => 'fas fa-user-plus']); $event->addAction('multi-user', ['title' => 'create-timesheet-multiuser', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form', 'icon' => 'fas fa-user-plus']);
} }
if ($this->isGranted('export_other_timesheet')) { if ($this->isGranted('export_other_timesheet')) {

View File

@@ -31,7 +31,7 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('edit', $user)) { if ($this->isGranted('edit', $user)) {
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats', 'translation_domain' => 'actions']); $event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats']);
} }
if ($this->isGranted('preferences', $user)) { if ($this->isGranted('preferences', $user)) {
$event->addConfig($this->path('user_profile_preferences', ['username' => $user->getUserIdentifier()])); $event->addConfig($this->path('user_profile_preferences', ['username' => $user->getUserIdentifier()]));

View File

@@ -40,7 +40,7 @@ final class UserSubscriber extends AbstractActionsSubscriber
} }
if ($this->isGranted('view', $user)) { if ($this->isGranted('view', $user)) {
$event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUserIdentifier()]), 'translation_domain' => 'actions', 'title' => 'profile-stats']); $event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats']);
$event->addDivider(); $event->addDivider();
} }
@@ -62,7 +62,7 @@ final class UserSubscriber extends AbstractActionsSubscriber
} }
if ($user->isEnabled() && $this->isGranted('view_other_timesheet')) { if ($user->isEnabled() && $this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['url' => $this->path('admin_timesheet', ['users[]' => $user->getId()]), 'title' => 'timesheet.filter', 'translation_domain' => 'actions']); $event->addActionToSubmenu('filter', 'timesheet', ['url' => $this->path('admin_timesheet', ['users[]' => $user->getId()]), 'title' => 'timesheet.filter']);
} }
if ($this->isGranted('view_team')) { if ($this->isGranted('view_team')) {

View File

@@ -28,7 +28,7 @@ final class AnnotationExtractor implements ExtractorInterface
/** /**
* @param string $value * @param string $value
* @return ColumnDefinition[] * @return list<ColumnDefinition>
* @throws ExtractorException * @throws ExtractorException
*/ */
public function extract($value): array public function extract($value): array
@@ -154,11 +154,7 @@ final class AnnotationExtractor implements ExtractorInterface
} }
} }
foreach ($columns as $name => $definition) { $columns = array_filter($columns, function ($value) { return $value !== null; });
if (null === $definition) {
unset($columns[$name]);
}
}
return array_values($columns); return array_values($columns);
} }

View File

@@ -29,6 +29,7 @@ final class MultiUpdateTable extends AbstractType
$builder->add('entities', HiddenType::class, [ $builder->add('entities', HiddenType::class, [
'required' => false, 'required' => false,
'attr' => ['class' => 'multi_update_ids']
]); ]);
$builder->get('entities')->addModelTransformer( $builder->get('entities')->addModelTransformer(

View File

@@ -38,6 +38,8 @@ final class ActivityType extends AbstractType
public function groupBy(Activity $activity, $key, $index): string public function groupBy(Activity $activity, $key, $index): string
{ {
if (null === $activity->getProject()) { if (null === $activity->getProject()) {
// this creates a optgroup with an empty title. previously this was null, which resulted in options without optgroup
// and those are ordered by Tomselect at the top - so globals always came first, see #4674
return ''; return '';
} }

View File

@@ -13,6 +13,7 @@ use App\Configuration\MailConfiguration;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\RawMessage; use Symfony\Component\Mime\RawMessage;
@@ -31,7 +32,11 @@ final class KimaiMailer implements MailerInterface
} }
if (\count($message->getFrom()) === 0) { if (\count($message->getFrom()) === 0) {
$message->from($this->configuration->getFromAddress()); $fallback = $this->configuration->getFromAddress();
if ($fallback === null) {
throw new \RuntimeException('Missing email "from" address');
}
$message->from(new Address($fallback, 'Kimai'));
} }
$this->mailer->send($message); $this->mailer->send($message);

View File

@@ -17,7 +17,7 @@ use DateTimeInterface;
final class MonthlyStatistic implements DateStatisticInterface final class MonthlyStatistic implements DateStatisticInterface
{ {
/** /**
* @var array<string|int, array<int<1, 12>, StatisticDate>> * @var array<string, array<int<1, 12>, StatisticDate>>
*/ */
private array $years = []; private array $years = [];
private DateTimeInterface $begin; private DateTimeInterface $begin;
@@ -50,7 +50,7 @@ final class MonthlyStatistic implements DateStatisticInterface
$day = (int) $begin->format('d'); $day = (int) $begin->format('d');
while ($tmp < $this->end) { while ($tmp < $this->end) {
$curYear = $tmp->format('Y'); $curYear = (string) $tmp->format('Y');
if (!isset($years[$curYear])) { if (!isset($years[$curYear])) {
$year = []; $year = [];
for ($i = 1; $i < 13; $i++) { for ($i = 1; $i < 13; $i++) {
@@ -67,7 +67,7 @@ final class MonthlyStatistic implements DateStatisticInterface
} }
$tmp->modify('+1 month'); $tmp->modify('+1 month');
} }
$this->years = $years; $this->years = $years; // @phpstan-ignore assign.propertyType
} }
/** /**

View File

@@ -13,6 +13,7 @@ use App\Entity\Project;
use App\Repository\ActivityRateRepository; use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\ProjectRateRepository; use App\Repository\ProjectRateRepository;
use App\Repository\Query\ActivityQuery;
final class ProjectDuplicationService final class ProjectDuplicationService
{ {
@@ -53,7 +54,11 @@ final class ProjectDuplicationService
$this->projectRateRepository->saveRate($newRate); $this->projectRateRepository->saveRate($newRate);
} }
$allActivities = $this->activityRepository->findByProject($project); $query = new ActivityQuery();
$query->addProject($project);
$query->setExcludeGlobals(true);
$allActivities = $this->activityRepository->getActivitiesForQuery($query);
foreach ($allActivities as $activity) { foreach ($allActivities as $activity) {
$newActivity = clone $activity; $newActivity = clone $activity;
$newActivity->setProject($newProject); $newActivity->setProject($newProject);

View File

@@ -37,18 +37,6 @@ class ActivityRepository extends EntityRepository
{ {
use RepositorySearchTrait; use RepositorySearchTrait;
/**
* @param Project $project
* @return array<Activity>
*/
public function findByProject(Project $project): array
{
$query = new ActivityQuery();
$query->addProject($project);
return $this->getActivitiesForQuery($query);
}
/** /**
* @param int[] $activityIds * @param int[] $activityIds
* @return array<Activity> * @return array<Activity>

View File

@@ -64,7 +64,7 @@ trait RepositorySearchTrait
$c = 0; $c = 0;
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
$and = $qb->expr()->andX(); $and = $qb->expr()->andX();
/** @var non-falsy-string&literal-string $alias */ /** @var non-falsy-string&lowercase-string $alias */
$alias = 'meta' . $a++; $alias = 'meta' . $a++;
$paramName = 'metaName' . $i++; $paramName = 'metaName' . $i++;
$paramValue = 'metaValue' . $c++; $paramValue = 'metaValue' . $c++;

View File

@@ -108,8 +108,6 @@ final class TimesheetService
} }
/** /**
* @param Timesheet $timesheet
* @return Timesheet
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped * @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
* @throws InvalidArgumentException for already persisted timesheets * @throws InvalidArgumentException for already persisted timesheets
* @throws AccessDeniedException if user is not allowed to start timesheet * @throws AccessDeniedException if user is not allowed to start timesheet
@@ -233,11 +231,10 @@ final class TimesheetService
} }
/** /**
* @param Timesheet $timesheet
* @param string[] $groups * @param string[] $groups
* @throws ValidationFailedException * @throws ValidationFailedException
*/ */
private function validateTimesheet(Timesheet $timesheet, array $groups = []): void public function validateTimesheet(Timesheet $timesheet, array $groups = []): void
{ {
$errors = $this->validator->validate($timesheet, null, $groups); $errors = $this->validator->validate($timesheet, null, $groups);

View File

@@ -19,6 +19,7 @@ use App\Utils\LocaleFormatter;
use DateTime; use DateTime;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
@@ -41,7 +42,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
new TwigFilter('date_short', [$this, 'dateShort']), new TwigFilter('date_short', [$this, 'dateShort']),
new TwigFilter('date_time', [$this, 'dateTime']), new TwigFilter('date_time', [$this, 'dateTime']),
// cannot be deleted right now, needs to be kept for invoice and export templates // cannot be deleted right now, needs to be kept for invoice and export templates
new TwigFilter('date_full', [$this, 'dateTime'], ['deprecated' => true, 'alternative' => 'date_time']), new TwigFilter('date_full', [$this, 'dateTime'], ['deprecation_info' => new DeprecatedCallableInfo('Kimai', '2.0', 'date_time')]),
new TwigFilter('date_format', [$this, 'dateFormat']), new TwigFilter('date_format', [$this, 'dateFormat']),
new TwigFilter('date_weekday', [$this, 'dateWeekday']), new TwigFilter('date_weekday', [$this, 'dateWeekday']),
new TwigFilter('time', [$this, 'time']), new TwigFilter('time', [$this, 'time']),

View File

@@ -31,7 +31,7 @@ final class LocaleFormatter
private ?NumberFormatter $moneyFormatter = null; private ?NumberFormatter $moneyFormatter = null;
private ?NumberFormatter $moneyFormatterNoCurrency = null; private ?NumberFormatter $moneyFormatterNoCurrency = null;
public function __construct(private LocaleService $localeService, private string $locale) public function __construct(private readonly LocaleService $localeService, private readonly string $locale)
{ {
} }

View File

@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use App\Form\Model\MultiUserTimesheet;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
@@ -35,6 +36,10 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
throw new UnexpectedTypeException($value, TimesheetEntity::class); throw new UnexpectedTypeException($value, TimesheetEntity::class);
} }
if ($value instanceof MultiUserTimesheet) {
return;
}
$begin = $value->getBegin(); $begin = $value->getBegin();
$end = $value->getEnd(); $end = $value->getEnd();

View File

@@ -28,13 +28,13 @@ final class WorkingTimeCalculatorDay implements WorkingTimeCalculator
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
{ {
return (int) match ($dateTime->format('N')) { return (int) match ($dateTime->format('N')) {
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0), '1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0, false),
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0), '2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0, false),
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0), '3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0, false),
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0), '4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0, false),
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0), '5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0, false),
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0), '6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0, false),
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0), '7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0, false),
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d')) default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
}; };
} }

View File

@@ -18,7 +18,7 @@ use App\Model\Month as BaseMonth;
*/ */
final class Month extends BaseMonth final class Month extends BaseMonth
{ {
public function __construct(\DateTimeImmutable $month, private User $user) public function __construct(\DateTimeImmutable $month, private readonly User $user)
{ {
parent::__construct($month); parent::__construct($month);
} }

View File

@@ -503,18 +503,6 @@
"ref": "179470cb6492db92dffee208cfdb436f175c93b4" "ref": "179470cb6492db92dffee208cfdb436f175c93b4"
} }
}, },
"symfony/polyfill-intl-grapheme": {
"version": "v1.13.1"
},
"symfony/polyfill-intl-icu": {
"version": "v1.6.0"
},
"symfony/polyfill-intl-idn": {
"version": "v1.11.0"
},
"symfony/polyfill-intl-normalizer": {
"version": "v1.13.1"
},
"symfony/process": { "symfony/process": {
"version": "v4.0.3" "version": "v4.0.3"
}, },

View File

@@ -3,7 +3,7 @@
{% block main %} {% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = { {% set formOptions = {
'title': (activity.id is null ? 'create-activity'|trans({}, 'actions') : 'edit'|trans({}, 'actions')), 'title': (activity.id is null ? 'create-activity'|trans : 'edit'|trans),
'form': form, 'form': form,
'back': path('admin_activity') 'back': path('admin_activity')
} %} } %}

View File

@@ -2,7 +2,7 @@
{% block main %} {% block main %}
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ activity.name, 'title': ('permissions'|trans) ~ ': ' ~ activity.name,
'form': form, 'form': form,
'back': path('activity_details', {'id': activity.id}) 'back': path('activity_details', {'id': activity.id})
}) }} }) }}

View File

@@ -3,7 +3,7 @@
{% block main %} {% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = { {% set formOptions = {
'title': (customer.id is null ? 'create'|trans : 'edit'|trans({}, 'actions')), 'title': (customer.id is null ? 'create'|trans : 'edit'|trans),
'form': form, 'form': form,
'back': path('admin_customer') 'back': path('admin_customer')
} %} } %}

View File

@@ -2,7 +2,7 @@
{% block main %} {% block main %}
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ customer.name, 'title': ('permissions'|trans) ~ ': ' ~ customer.name,
'form': form, 'form': form,
'back': path('customer_details', {'id': customer.id}) 'back': path('customer_details', {'id': customer.id})
}) }} }) }}

View File

@@ -49,9 +49,9 @@
<h3 class="card-title">{{ widget.title|trans({}, widget.translationDomain) }}</h3> <h3 class="card-title">{{ widget.title|trans({}, widget.translationDomain) }}</h3>
<div class="card-actions"> <div class="card-actions">
{% if widget.hasForm() %} {% if widget.hasForm() %}
{{ card_tool_button('configuration', {'title': 'settings', 'translation_domain': 'actions', 'class': 'modal-ajax-form', 'url': path('tasks_create')}) }} {{ card_tool_button('configuration', {'title': 'settings', 'class': 'modal-ajax-form', 'url': path('tasks_create')}) }}
{% endif %} {% endif %}
{{ card_tool_button('delete', {'title': 'widget_remove', 'translation_domain': 'actions', 'url': '#', 'onclick': "removeWidget(this, '" ~ widget.id ~ "'); return false;"}) }} {{ card_tool_button('delete', {'title': 'widget_remove', 'url': '#', 'onclick': "removeWidget(this, '" ~ widget.id ~ "'); return false;"}) }}
</div> </div>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">

View File

@@ -25,7 +25,7 @@
<input type="submit" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" value="{{ (submit_button|default('action.save'))|trans }}" class="btn btn-primary" /> <input type="submit" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" value="{{ (submit_button|default('action.save'))|trans }}" class="btn btn-primary" />
{% endblock %} {% endblock %}
{% if _back is not same as (false) %} {% if _back is not same as (false) %}
<a href="{{ _back }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a> <a href="{{ _back }}" class="btn btn-link">{{ 'back'|trans }}</a>
{% endif %} {% endif %}
{% if _reset is not same as (false) %} {% if _reset is not same as (false) %}
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" /> <input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />

View File

@@ -24,7 +24,7 @@
{% block box_footer %} {% block box_footer %}
<input type="submit" value="{{ 'delete'|trans }}" class="btn btn-danger" /> <input type="submit" value="{{ 'delete'|trans }}" class="btn btn-danger" />
{% if back %} {% if back %}
<a href="{{ back }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a> <a href="{{ back }}" class="btn btn-link">{{ 'back'|trans }}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block box_after %} {% block box_after %}

View File

@@ -19,7 +19,7 @@
{% endblock %} {% endblock %}
{% block box_attributes %}id="team_listing_box"{% endblock %} {% block box_attributes %}id="team_listing_box"{% endblock %}
{% block box_title %} {% block box_title %}
{{ 'permissions'|trans({}, 'actions') }} {{ 'permissions'|trans }}
{% if teams|length > 0 %}<small class="text-body-secondary d-none d-sm-inline ms-1">{{ 'team.visibility_restricted'|trans({}, 'teams') }}</small>{% endif %} {% if teams|length > 0 %}<small class="text-body-secondary d-none d-sm-inline ms-1">{{ 'team.visibility_restricted'|trans({}, 'teams') }}</small>{% endif %}
{% endblock %} {% endblock %}
{% block box_body_class %}{% if teams|length > 0 %}p-0{% endif %}{% endblock %} {% block box_body_class %}{% if teams|length > 0 %}p-0{% endif %}{% endblock %}

View File

@@ -286,7 +286,7 @@
<form class="w-100 d-print-none row row-cards p-3"> <form class="w-100 d-print-none row row-cards p-3">
<div class="card mb-1 p-0"> <div class="card mb-1 p-0">
<div class="card-header"> <div class="card-header">
{{ 'settings'|trans({}, 'actions') }} {{ 'settings'|trans }}
</div> </div>
<div class="card-body"> <div class="card-body">
<label for="duration-decimal"> <label for="duration-decimal">

View File

@@ -27,7 +27,7 @@
{% endblock %} {% endblock %}
{% block box_footer %} {% block box_footer %}
<button type="submit" class="btn btn-primary">{{ 'action.save'|trans }}</button> <button type="submit" class="btn btn-primary">{{ 'action.save'|trans }}</button>
<a href="{{ path('admin_invoice_template') }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a> <a href="{{ path('admin_invoice_template') }}" class="btn btn-link">{{ 'back'|trans }}</a>
{% endblock %} {% endblock %}
{% endembed %} {% endembed %}
{% elseif upload_error is not null %} {% elseif upload_error is not null %}

View File

@@ -224,7 +224,7 @@
{% else %} {% else %}
{% if headerOptions.batchUpdate is defined %} {% if headerOptions.batchUpdate is defined %}
<input type="checkbox" id="multi_update_all" class="multiupdater form-check-input m-0 align-middle" title="{{ 'batch_table_checkbox_all'|trans }}"> <input type="checkbox" id="multi_update_all_{{ tableName }}" class="multi_update_all multiupdater form-check-input m-0 align-middle" title="{{ 'batch_table_checkbox_all'|trans }}">
{% endif %} {% endif %}
{{ headerTitle }} {{ headerTitle }}
{% if headerOptions.html_after is defined %} {% if headerOptions.html_after is defined %}
@@ -250,7 +250,7 @@
{% if (route is not empty and entries is not null) or multi_update_form is not null %} {% if (route is not empty and entries is not null) or multi_update_form is not null %}
<div class="card-footer d-flex align-items-center"> <div class="card-footer d-flex align-items-center">
{% if multi_update_form is not null %} {% if multi_update_form is not null %}
{{ form_start(multi_update_form, {'attr': {'id': 'multi_update_form', 'style': 'display:none', 'data-question': 'update_multiple'|trans}}) }} {{ form_start(multi_update_form, {'attr': {'class': 'multi_update_form', 'style': 'display:none', 'data-question': 'update_multiple'|trans}}) }}
{% for formChild in multi_update_form.children %} {% for formChild in multi_update_form.children %}
{{ form_widget(formChild) }} {{ form_widget(formChild) }}
{% endfor %} {% endfor %}

View File

@@ -63,7 +63,7 @@
{% if values.children is defined and values.children|length > 0 %} {% if values.children is defined and values.children|length > 0 %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn {{ btnClasses }} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn {{ btnClasses }} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans({}, 'actions') }}{% endif %} {{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans }}{% endif %}
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
{%- for icon, values in values.children %} {%- for icon, values in values.children %}

View File

@@ -3,7 +3,7 @@
{% block main %} {% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = { {% set formOptions = {
'title': (project.id is null ? 'create-project'|trans({}, 'actions') : 'edit'|trans({}, 'actions')), 'title': (project.id is null ? 'create-project'|trans : 'edit'|trans),
'form': form, 'form': form,
'back': path('admin_project') 'back': path('admin_project')
} %} } %}

View File

@@ -2,7 +2,7 @@
{% block main %} {% block main %}
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', { {{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ project.name, 'title': ('permissions'|trans) ~ ': ' ~ project.name,
'form': form, 'form': form,
'back': path('project_details', {'id': project.id}) 'back': path('project_details', {'id': project.id})
}) }} }) }}

View File

@@ -4,7 +4,7 @@
{% block main %} {% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = { {% set formOptions = {
'title': (tag.id is null ? 'create'|trans : 'edit'|trans({}, 'actions')), 'title': (tag.id is null ? 'create'|trans : 'edit'|trans),
'form': form, 'form': form,
'back': path('tags') 'back': path('tags')
} %} } %}

View File

@@ -91,9 +91,9 @@
{% block datatable_column %} {% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}{% if column == 'description' %} timesheet-description{% endif %}"> <td class="{{ tables.class(dataTable, column) }}{% if column == 'description' %} timesheet-description{% endif %}">
{% if column == 'id' %} {% if column == 'id' %}
{% if is_granted('edit', entry) or is_granted('delete', entry) %} {% if is_granted('edit', entry) or is_granted('delete', entry) %}
{{ tables.datatable_multiupdate_row(entry.id) }} {{ tables.datatable_multiupdate_row(entry.id) }}
{% endif %} {% endif %}
{% elseif column == 'date' %} {% elseif column == 'date' %}
{{ entry.begin|date_short }} {{ entry.begin|date_short }}
{% elseif column == 'starttime' %} {% elseif column == 'starttime' %}

View File

@@ -4,7 +4,7 @@
{% block main %} {% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = { {% set formOptions = {
'title': (access_token.id is null ? 'create'|trans : 'edit'|trans({}, 'actions')), 'title': (access_token.id is null ? 'create'|trans : 'edit'|trans),
'form': form, 'form': form,
'back': path('user_profile_access_token', {'username': user.userIdentifier}) 'back': path('user_profile_access_token', {'username': user.userIdentifier})
} %} } %}

View File

@@ -225,7 +225,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
if (\count($globalError) > 0) { if (\count($globalError) > 0) {
self::assertArrayHasKey('errors', $result['errors']); self::assertArrayHasKey('errors', $result['errors']);
foreach ($globalError as $err) { foreach ($globalError as $err) {
self::assertTrue(\in_array($err, $result['errors']['errors']), 'Missing global validation error: ' . $err); self::assertTrue(\in_array($err, $result['errors']['errors']), 'Missing global validation error: ' . $err); // @phpstan-ignore binaryOp.invalid
} }
} }

View File

@@ -60,11 +60,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
foreach ($result as $item) { foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item); self::assertApiResponseTypeStructure('PageActionItem', $item);
} }
$i = 0; $i = 0;
foreach ($entries as $id) { foreach ($entries as $id) {
self::assertIsArray($result[$i]);
self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view)); self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view));
$i++; $i++;
} }
@@ -110,11 +112,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
foreach ($result as $item) { foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item); self::assertApiResponseTypeStructure('PageActionItem', $item);
} }
$i = 0; $i = 0;
foreach ($entries as $id) { foreach ($entries as $id) {
self::assertIsArray($result[$i]);
self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view)); self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view));
$i++; $i++;
} }
@@ -159,11 +163,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
foreach ($result as $item) { foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item); self::assertApiResponseTypeStructure('PageActionItem', $item);
} }
$i = 0; $i = 0;
foreach ($entries as $id) { foreach ($entries as $id) {
self::assertIsArray($result[$i]);
self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view)); self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view));
$i++; $i++;
} }
@@ -204,11 +210,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
foreach ($result as $item) { foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item); self::assertApiResponseTypeStructure('PageActionItem', $item);
} }
$i = 0; $i = 0;
foreach ($entries as $id) { foreach ($entries as $id) {
self::assertIsArray($result[$i]);
self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view)); self::assertEquals($id, $result[$i]['id'], \sprintf('Failed action "%s" with name "%s" in view "%s"', $i, $id, $view));
$i++; $i++;
} }

View File

@@ -21,6 +21,7 @@ use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\Query\VisibilityInterface; use App\Repository\Query\VisibilityInterface;
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock; use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
use Symfony\Component\EventDispatcher\EventDispatcher;
/** /**
* @group integration * @group integration
@@ -179,6 +180,7 @@ class ActivityControllerTest extends APIControllerBaseTest
for ($i = 0; $i < \count($result); $i++) { for ($i = 0; $i < \count($result); $i++) {
$activity = $result[$i]; $activity = $result[$i];
$hasProject = $expected[$i][0]; $hasProject = $expected[$i][0];
self::assertIsArray($activity);
self::assertApiResponseTypeStructure('ActivityCollection', $activity); self::assertApiResponseTypeStructure('ActivityCollection', $activity);
if ($hasProject && $projectId !== null) { if ($hasProject && $projectId !== null) {
$this->assertEquals($projectId, $activity['project']); $this->assertEquals($projectId, $activity['project']);
@@ -219,6 +221,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(5, \count($result)); $this->assertEquals(5, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('ActivityCollection', $result[0]); self::assertApiResponseTypeStructure('ActivityCollection', $result[0]);
$this->assertEquals($imports[0]->getId(), $result[4]['project']); $this->assertEquals($imports[0]->getId(), $result[4]['project']);
$this->assertEquals($imports[1]->getId(), $result[3]['project']); $this->assertEquals($imports[1]->getId(), $result[3]['project']);
@@ -429,7 +432,9 @@ class ActivityControllerTest extends APIControllerBaseTest
public function testMetaAction(): void public function testMetaAction(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
static::getContainer()->get('event_dispatcher')->addSubscriber(new ActivityTestMetaFieldSubscriberMock()); /** @var EventDispatcher $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->addSubscriber(new ActivityTestMetaFieldSubscriberMock());
$data = [ $data = [
'name' => 'metatestmock', 'name' => 'metatestmock',

View File

@@ -20,7 +20,6 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
/** /**
* @covers \App\API\Authentication\TokenAuthenticator * @covers \App\API\Authentication\TokenAuthenticator
@@ -118,7 +117,6 @@ class TokenAuthenticatorTest extends TestCase
$request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo2', 'HTTP_X-AUTH-TOKEN' => 'bar']); $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo2', 'HTTP_X-AUTH-TOKEN' => 'bar']);
$passport = $sut->authenticate($request); $passport = $sut->authenticate($request);
self::assertInstanceOf(Passport::class, $passport);
$badge = $passport->getBadge(UserBadge::class); $badge = $passport->getBadge(UserBadge::class);
self::assertInstanceOf(UserBadge::class, $badge); self::assertInstanceOf(UserBadge::class, $badge);
self::assertEquals('foo2', $badge->getUserIdentifier()); self::assertEquals('foo2', $badge->getUserIdentifier());

View File

@@ -21,6 +21,7 @@ use App\Entity\User;
use App\Repository\CustomerRateRepository; use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock; use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
use Symfony\Component\EventDispatcher\EventDispatcher;
/** /**
* @group integration * @group integration
@@ -104,6 +105,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]); self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
} }
@@ -120,6 +122,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]); self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
} }
@@ -372,7 +375,9 @@ class CustomerControllerTest extends APIControllerBaseTest
public function testMetaAction(): void public function testMetaAction(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
self::getContainer()->get('event_dispatcher')->addSubscriber(new CustomerTestMetaFieldSubscriberMock()); /** @var EventDispatcher $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->addSubscriber(new CustomerTestMetaFieldSubscriberMock());
$data = [ $data = [
'name' => 'metatestmock', 'name' => 'metatestmock',

View File

@@ -51,6 +51,7 @@ class InvoiceControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(10, \count($result)); $this->assertEquals(10, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]); self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
} }
@@ -72,6 +73,7 @@ class InvoiceControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(7, \count($result)); $this->assertEquals(7, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]); self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
} }
@@ -91,6 +93,7 @@ class InvoiceControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(4, \count($result)); $this->assertEquals(4, \count($result));
$this->assertPagination($client->getResponse(), 2, 4, 5, 20); $this->assertPagination($client->getResponse(), 2, 4, 5, 20);
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]); self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
} }

View File

@@ -21,6 +21,7 @@ use App\Repository\ProjectRateRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\VisibilityInterface; use App\Repository\Query\VisibilityInterface;
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock; use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\HttpKernelBrowser;
/** /**
@@ -105,6 +106,7 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('ProjectCollection', $result[0]); self::assertApiResponseTypeStructure('ProjectCollection', $result[0]);
} }
@@ -219,6 +221,7 @@ class ProjectControllerTest extends APIControllerBaseTest
for ($i = 0; $i < \count($expected); $i++) { for ($i = 0; $i < \count($expected); $i++) {
$project = $result[$i]; $project = $result[$i];
self::assertIsArray($project);
self::assertApiResponseTypeStructure('ProjectCollection', $project); self::assertApiResponseTypeStructure('ProjectCollection', $project);
if ($customerId !== null) { if ($customerId !== null) {
$this->assertEquals($customerId, $project['customer']); $this->assertEquals($customerId, $project['customer']);
@@ -579,7 +582,9 @@ class ProjectControllerTest extends APIControllerBaseTest
public function testMetaAction(): void public function testMetaAction(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
self::getContainer()->get('event_dispatcher')->addSubscriber(new ProjectTestMetaFieldSubscriberMock()); /** @var EventDispatcher $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->addSubscriber(new ProjectTestMetaFieldSubscriberMock());
$data = [ $data = [
'name' => 'metatestmock', 'name' => 'metatestmock',

View File

@@ -150,7 +150,13 @@ trait RateControllerTestTrait
$this->assertEquals(\count($expectedRates), \count($result)); $this->assertEquals(\count($expectedRates), \count($result));
foreach ($result as $rate) { foreach ($result as $rate) {
$this->assertRateStructure($rate, ($rate['user'] === null ? null : $rate['user']['id'])); $this->assertIsArray($rate);
if ($rate['user'] === null) {
$this->assertRateStructure($rate);
} else {
$this->assertIsArray($rate['user']);
$this->assertRateStructure($rate, $rate['user']['id']);
}
} }
} }

View File

@@ -66,6 +66,7 @@ class TagControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(3, \count($result)); $this->assertEquals(3, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TagEntity', $result[0]); self::assertApiResponseTypeStructure('TagEntity', $result[0]);
} }

View File

@@ -65,6 +65,7 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
self::assertEquals(2, \count($result)); self::assertEquals(2, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TeamCollection', $result[0]); self::assertApiResponseTypeStructure('TeamCollection', $result[0]);
} }
@@ -152,7 +153,9 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$updateId = $result['id']; $updateId = $result['id'];
$this->assertIsNumeric($updateId);
$data = [ $data = [
'name' => 'foo', 'name' => 'foo',
@@ -171,14 +174,17 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
self::assertCount(3, $result['members']); self::assertCount(3, $result['members']);
$this->assertIsNumeric($updateId);
$this->request($client, '/api/teams/' . $updateId); $this->request($client, '/api/teams/' . $updateId);
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['members']);
self::assertCount(3, $result['members']); self::assertCount(3, $result['members']);
$this->assertIsArray($result['members'][1]);
self::assertFalse($result['members'][1]['teamlead']); self::assertFalse($result['members'][1]['teamlead']);
self::assertEquals(1, $result['members'][1]['user']['id']); self::assertEquals(1, $result['members'][1]['user']['id']);
self::assertEquals('clara_customer', $result['members'][1]['user']['username']); self::assertEquals('clara_customer', $result['members'][1]['user']['username']);
@@ -187,7 +193,7 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertEquals(4, $result['members'][2]['user']['id']); self::assertEquals(4, $result['members'][2]['user']['id']);
self::assertEquals('tony_teamlead', $result['members'][2]['user']['username']); self::assertEquals('tony_teamlead', $result['members'][2]['user']['username']);
self::assertTrue(true, $result['members'][0]['teamlead']); self::assertTrue($result['members'][0]['teamlead']);
self::assertEquals(2, $result['members'][0]['user']['id']); self::assertEquals(2, $result['members'][0]['user']['id']);
self::assertEquals('john_user', $result['members'][0]['user']['username']); self::assertEquals('john_user', $result['members'][0]['user']['username']);
} }
@@ -204,6 +210,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$data = [ $data = [
'name' => '1', 'name' => '1',
@@ -230,6 +238,7 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']); $this->assertNotEmpty($result['id']);
$id = $result['id']; $id = $result['id'];
$this->assertIsNumeric($id);
$this->request($client, '/api/teams/' . $id, 'DELETE'); $this->request($client, '/api/teams/' . $id, 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
@@ -249,7 +258,10 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['members']);
self::assertCount(1, $result['members']); self::assertCount(1, $result['members']);
$this->assertIsNumeric($result['id']);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
@@ -257,6 +269,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['members']);
self::assertCount(2, $result['members']); self::assertCount(2, $result['members']);
} }
@@ -273,6 +286,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertEntityNotFoundForPost($client, '/api/teams/999/members/999'); $this->assertEntityNotFoundForPost($client, '/api/teams/999/members/999');
@@ -303,6 +318,9 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$this->assertIsArray($result['members']);
self::assertCount(4, $result['members']); self::assertCount(4, $result['members']);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'DELETE'); $this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'DELETE');
@@ -311,6 +329,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['members']);
self::assertCount(3, $result['members']); self::assertCount(3, $result['members']);
} }
@@ -329,6 +348,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertNotFoundForDelete($client, '/api/teams/999/members/999'); $this->assertNotFoundForDelete($client, '/api/teams/999/members/999');
@@ -359,6 +380,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['customers']); self::assertCount(0, $result['customers']);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
@@ -367,7 +390,9 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['customers']);
self::assertCount(1, $result['customers']); self::assertCount(1, $result['customers']);
$this->assertIsArray($result['customers'][0]);
self::assertEquals(1, $result['customers'][0]['id']); self::assertEquals(1, $result['customers'][0]['id']);
} }
@@ -384,6 +409,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertEntityNotFoundForPost($client, '/api/teams/999/customers/999'); $this->assertEntityNotFoundForPost($client, '/api/teams/999/customers/999');
@@ -395,6 +422,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['customers']); self::assertCount(1, $result['customers']);
// cannot add existing customer // cannot add existing customer
@@ -416,12 +445,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['customers']); self::assertCount(0, $result['customers']);
// add customer // add customer
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['customers']); self::assertCount(1, $result['customers']);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'DELETE'); $this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'DELETE');
@@ -429,7 +462,10 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['customers']);
self::assertCount(0, $result['customers']); self::assertCount(0, $result['customers']);
/** @var EntityManager $em */ /** @var EntityManager $em */
@@ -454,6 +490,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertNotFoundForDelete($client, '/api/teams/999/customers/999'); $this->assertNotFoundForDelete($client, '/api/teams/999/customers/999');
@@ -477,6 +515,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['projects']); self::assertCount(0, $result['projects']);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
@@ -484,7 +524,9 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['projects']);
self::assertCount(1, $result['projects']); self::assertCount(1, $result['projects']);
$this->assertIsArray($result['projects'][0]);
self::assertEquals(1, $result['projects'][0]['id']); self::assertEquals(1, $result['projects'][0]['id']);
} }
@@ -501,6 +543,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertEntityNotFoundForPost($client, '/api/teams/999/projects/999'); $this->assertEntityNotFoundForPost($client, '/api/teams/999/projects/999');
@@ -514,6 +558,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['projects']); self::assertCount(1, $result['projects']);
// cannot add existing project // cannot add existing project
@@ -535,12 +581,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['projects']); self::assertCount(0, $result['projects']);
// add project // add project
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['projects']);
self::assertCount(1, $result['projects']); self::assertCount(1, $result['projects']);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'DELETE'); $this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'DELETE');
@@ -549,6 +599,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['projects']);
self::assertCount(0, $result['projects']); self::assertCount(0, $result['projects']);
/** @var EntityManager $em */ /** @var EntityManager $em */
@@ -573,6 +624,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertNotFoundForDelete($client, '/api/teams/999/projects/999'); $this->assertNotFoundForDelete($client, '/api/teams/999/projects/999');
@@ -596,6 +649,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['activities']); self::assertCount(0, $result['activities']);
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
@@ -603,7 +658,9 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['activities']);
self::assertCount(1, $result['activities']); self::assertCount(1, $result['activities']);
$this->assertIsArray($result['activities'][0]);
self::assertEquals(1, $result['activities'][0]['id']); self::assertEquals(1, $result['activities'][0]['id']);
} }
@@ -620,6 +677,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertEntityNotFoundForPost($client, '/api/teams/999/activities/999'); $this->assertEntityNotFoundForPost($client, '/api/teams/999/activities/999');
@@ -631,6 +690,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['activities']); self::assertCount(1, $result['activities']);
// cannot add existing activity // cannot add existing activity
@@ -652,12 +713,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['activities']);
self::assertCount(0, $result['activities']); self::assertCount(0, $result['activities']);
// add activity // add activity
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST'); $this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['activities']); self::assertCount(1, $result['activities']);
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'DELETE'); $this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'DELETE');
@@ -666,6 +731,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result); $this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result); self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['activities']);
self::assertCount(0, $result['activities']); self::assertCount(0, $result['activities']);
} }
@@ -684,6 +750,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data)); $this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true); $result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found // team not found
$this->assertNotFoundForDelete($client, '/api/teams/999/activities/9999'); $this->assertNotFoundForDelete($client, '/api/teams/999/activities/9999');

View File

@@ -20,6 +20,7 @@ use App\Entity\User;
use App\Tests\DataFixtures\TimesheetFixtures; use App\Tests\DataFixtures\TimesheetFixtures;
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock; use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
use App\Timesheet\DateTimeFactory; use App\Timesheet\DateTimeFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
/** /**
@@ -63,6 +64,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -79,6 +81,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
} }
@@ -103,6 +106,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['users' => [2]]; $query = ['users' => [2]];
@@ -115,6 +119,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -162,6 +167,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -186,6 +192,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(17, \count($result)); self::assertEquals(17, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -237,6 +244,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(4, \count($result)); self::assertEquals(4, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -289,6 +297,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(5, \count($result)); self::assertEquals(5, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -324,6 +333,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(7, \count($result)); self::assertEquals(7, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [ $query = [
@@ -342,6 +352,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [ $query = [
@@ -358,6 +369,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(17, \count($result)); self::assertEquals(17, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -896,6 +908,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
self::assertNotEmpty($result['id']); self::assertNotEmpty($result['id']);
self::assertIsNumeric($result['id']);
$id = $result['id']; $id = $result['id'];
$this->request($client, '/api/timesheets/' . $id, 'DELETE'); $this->request($client, '/api/timesheets/' . $id, 'DELETE');
@@ -999,6 +1012,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(1, \count($result)); self::assertEquals(1, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
} }
@@ -1024,6 +1038,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertEquals(3, \count($result)); self::assertEquals(3, \count($result));
foreach ($result as $timesheet) { foreach ($result as $timesheet) {
$this->assertIsArray($timesheet);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet);
} }
} }
@@ -1142,6 +1157,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Test', 'Admin']]; $query = ['tags' => ['Test', 'Admin']];
@@ -1154,6 +1170,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(10, \count($result)); self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Nothing-2-see', 'here']]; $query = ['tags' => ['Nothing-2-see', 'here']];
@@ -1166,6 +1183,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result); self::assertIsArray($result);
self::assertNotEmpty($result); self::assertNotEmpty($result);
self::assertEquals(20, \count($result)); self::assertEquals(20, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
} }
@@ -1190,6 +1208,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content); self::assertIsString($content);
$result = json_decode($content, true); $result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']); $this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']); $this->assertEmpty($result['tags']);
@@ -1227,6 +1246,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content); self::assertIsString($content);
$result = json_decode($content, true); $result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']); $this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']); $this->assertEmpty($result['tags']);
@@ -1272,6 +1292,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content); self::assertIsString($content);
$result = json_decode($content, true); $result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
self::assertEquals('foo', $result['description']); self::assertEquals('foo', $result['description']);
self::assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']); self::assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
@@ -1342,8 +1363,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertNotEmpty($result['id']); self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied $this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
self::assertEquals(2016, $result['rate']); self::assertEquals(2016, $result['rate']);
$id = $result['id'];
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH'); self::assertIsNumeric($id);
$this->request($client, '/api/timesheets/' . $id . '/duplicate', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -1380,6 +1402,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content); self::assertIsString($content);
$result = json_decode($content, true); $result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em->clear(); $em->clear();
@@ -1456,7 +1479,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER);
$id = $timesheets[0]->getId(); $id = $timesheets[0]->getId();
static::getContainer()->get('event_dispatcher')->addSubscriber(new TimesheetTestMetaFieldSubscriberMock()); /** @var EventDispatcher $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->addSubscriber(new TimesheetTestMetaFieldSubscriberMock());
$data = [ $data = [
'name' => 'metatestmock', 'name' => 'metatestmock',
@@ -1472,6 +1497,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content); self::assertIsString($content);
$result = json_decode($content, true); $result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result); self::assertApiResponseTypeStructure('TimesheetEntity', $result);
self::assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]); self::assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]);

View File

@@ -53,6 +53,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(7, \count($result)); $this->assertEquals(7, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
@@ -70,6 +71,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(7, \count($result)); $this->assertEquals(7, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserEntity', $user); self::assertApiResponseTypeStructure('UserEntity', $user);
} }
} }
@@ -86,6 +88,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(1, \count($result)); $this->assertEquals(1, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
@@ -102,6 +105,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals(8, \count($result)); $this->assertEquals(8, \count($result));
foreach ($result as $user) { foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user); self::assertApiResponseTypeStructure('UserCollection', $user);
} }
} }
@@ -283,7 +287,9 @@ class UserControllerTest extends APIControllerBaseTest
'ROLE_TEAMLEAD', 'ROLE_TEAMLEAD',
], ],
]; ];
$this->request($client, '/api/users/' . $result['id'], 'PATCH', [], json_encode($data)); $id = $result['id'];
self::assertIsNumeric($id);
$this->request($client, '/api/users/' . $id, 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();

View File

@@ -113,11 +113,7 @@ class ActivityServiceTest extends TestCase
{ {
$dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) { $dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
if ($event instanceof ActivityMetaDefinitionEvent) { if (!$event instanceof ActivityMetaDefinitionEvent && !$event instanceof ActivityCreateEvent) {
self::assertInstanceOf(Activity::class, $event->getEntity());
} elseif ($event instanceof ActivityCreateEvent) {
self::assertInstanceOf(Activity::class, $event->getActivity());
} else {
$this->fail('Invalid event received'); $this->fail('Invalid event received');
} }
@@ -136,11 +132,7 @@ class ActivityServiceTest extends TestCase
{ {
$dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) { $dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
if ($event instanceof ActivityCreatePreEvent) { if (!$event instanceof ActivityCreatePreEvent && !$event instanceof ActivityCreatePostEvent) {
self::assertInstanceOf(Activity::class, $event->getActivity());
} elseif ($event instanceof ActivityCreatePostEvent) {
self::assertInstanceOf(Activity::class, $event->getActivity());
} else {
$this->fail('Invalid event received'); $this->fail('Invalid event received');
} }

View File

@@ -13,6 +13,7 @@ use App\Command\ActivateUserCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
@@ -33,6 +34,7 @@ class ActivateUserCommandTest extends KernelTestCase
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var UserService $userService */
$userService = $container->get(UserService::class); $userService = $container->get(UserService::class);
$this->application->add(new ActivateUserCommand($userService)); $this->application->add(new ActivateUserCommand($userService));
@@ -71,8 +73,10 @@ class ActivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] User "chris_user" has been activated.', $output); $this->assertStringContainsString('[OK] User "chris_user" has been activated.', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('chris_user'); $user = $userRepository->loadUserByIdentifier('chris_user');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertTrue($user->isEnabled()); self::assertTrue($user->isEnabled());

View File

@@ -13,6 +13,7 @@ use App\Command\ChangePasswordCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
@@ -35,6 +36,7 @@ class ChangePasswordCommandTest extends KernelTestCase
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var UserService $userService */
$userService = $container->get(UserService::class); $userService = $container->get(UserService::class);
$this->application->add(new ChangePasswordCommand($userService)); $this->application->add(new ChangePasswordCommand($userService));
@@ -86,8 +88,10 @@ class ChangePasswordCommandTest extends KernelTestCase
$output = $commandTester->getDisplay(); $output = $commandTester->getDisplay();
$this->assertStringContainsString('[OK] Changed password for user "john_user".', $output); $this->assertStringContainsString('[OK] Changed password for user "john_user".', $output);
/** @var Registry $doctrine */
$doctrine = self::getContainer()->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = self::getContainer()->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user'); $user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);

View File

@@ -13,6 +13,7 @@ use App\Command\CreateUserCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@@ -31,10 +32,9 @@ class CreateUserCommandTest extends KernelTestCase
$kernel = self::bootKernel(); $kernel = self::bootKernel();
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var UserService $userService */
$this->application->add(new CreateUserCommand( $userService = $container->get(UserService::class);
$container->get(UserService::class), $this->application->add(new CreateUserCommand($userService));
));
} }
public function testCreateUserFailsForShortPassword(): void public function testCreateUserFailsForShortPassword(): void
@@ -53,11 +53,12 @@ class CreateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] Success! Created user: MyTestUser', $output); $this->assertStringContainsString('[OK] Success! Created user: MyTestUser', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('MyTestUser'); $user = $userRepository->loadUserByIdentifier('MyTestUser');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertNotNull($user);
} }
protected function createUser($username, $email, $role, $password): CommandTester protected function createUser($username, $email, $role, $password): CommandTester

View File

@@ -13,6 +13,7 @@ use App\Command\DeactivateUserCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
@@ -32,7 +33,7 @@ class DeactivateUserCommandTest extends KernelTestCase
$kernel = self::bootKernel(); $kernel = self::bootKernel();
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var UserService $userService */
$userService = $container->get(UserService::class); $userService = $container->get(UserService::class);
$this->application->add(new DeactivateUserCommand($userService)); $this->application->add(new DeactivateUserCommand($userService));
@@ -71,8 +72,10 @@ class DeactivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] User "john_user" has been deactivated.', $output); $this->assertStringContainsString('[OK] User "john_user" has been deactivated.', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user'); $user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertFalse($user->isEnabled()); self::assertFalse($user->isEnabled());

View File

@@ -13,6 +13,7 @@ use App\Command\DemoteUserCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
@@ -34,6 +35,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var UserService $userService */
$userService = $container->get(UserService::class); $userService = $container->get(UserService::class);
$this->application->add(new DemoteUserCommand($userService)); $this->application->add(new DemoteUserCommand($userService));
@@ -80,8 +82,10 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] Role "ROLE_TEAMLEAD" has been removed from user "tony_teamlead".', $output); $this->assertStringContainsString('[OK] Role "ROLE_TEAMLEAD" has been removed from user "tony_teamlead".', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('tony_teamlead'); $user = $userRepository->loadUserByIdentifier('tony_teamlead');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertFalse($user->hasTeamleadRole()); self::assertFalse($user->hasTeamleadRole());
@@ -95,8 +99,10 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] Super administrator role has been removed from the user "susan_super".', $output); $this->assertStringContainsString('[OK] Super administrator role has been removed from the user "susan_super".', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('susan_super'); $user = $userRepository->loadUserByIdentifier('susan_super');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertFalse($user->isSuperAdmin()); self::assertFalse($user->isSuperAdmin());

View File

@@ -45,6 +45,9 @@ class ExportCreateCommandTest extends KernelTestCase
if (is_dir($path)) { if (is_dir($path)) {
$files = glob($path . '*'); $files = glob($path . '*');
if ($files === false) {
return;
}
foreach ($files as $file) { foreach ($files as $file) {
unlink($file); unlink($file);
} }
@@ -71,12 +74,12 @@ class ExportCreateCommandTest extends KernelTestCase
$container = self::getContainer(); $container = self::getContainer();
$application->add(new ExportCreateCommand( $application->add(new ExportCreateCommand(
$container->get(ServiceExport::class), $container->get(ServiceExport::class), // @phpstan-ignore argument.type
$container->get(CustomerRepository::class), $container->get(CustomerRepository::class), // @phpstan-ignore argument.type
$container->get(ProjectRepository::class), $container->get(ProjectRepository::class), // @phpstan-ignore argument.type
$container->get(TeamRepository::class), $container->get(TeamRepository::class), // @phpstan-ignore argument.type
$container->get(UserRepository::class), $container->get(UserRepository::class), // @phpstan-ignore argument.type
$container->get(TranslatorInterface::class), $container->get(TranslatorInterface::class), // @phpstan-ignore argument.type
$mailer ?? $container->get(KimaiMailer::class), $mailer ?? $container->get(KimaiMailer::class),
)); ));

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Command; namespace App\Tests\Command;
use App\Command\InstallCommand; use App\Command\InstallCommand;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -27,9 +28,11 @@ class InstallCommandTest extends KernelTestCase
$kernel = self::bootKernel(); $kernel = self::bootKernel();
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
$this->application->add(new InstallCommand( $this->application->add(new InstallCommand(
$container->get('doctrine')->getConnection() $doctrine->getConnection() // @phpstan-ignore argument.type
)); ));
} }

View File

@@ -43,6 +43,9 @@ class InvoiceCreateCommandTest extends KernelTestCase
if (is_dir($path)) { if (is_dir($path)) {
$files = glob($path . '*'); $files = glob($path . '*');
if ($files === false) {
return;
}
foreach ($files as $file) { foreach ($files as $file) {
unlink($file); unlink($file);
} }
@@ -64,12 +67,12 @@ class InvoiceCreateCommandTest extends KernelTestCase
$container = self::getContainer(); $container = self::getContainer();
$this->application->add(new InvoiceCreateCommand( $this->application->add(new InvoiceCreateCommand(
$container->get(ServiceInvoice::class), $container->get(ServiceInvoice::class), // @phpstan-ignore argument.type
$container->get(CustomerRepository::class), $container->get(CustomerRepository::class), // @phpstan-ignore argument.type
$container->get(ProjectRepository::class), $container->get(ProjectRepository::class), // @phpstan-ignore argument.type
$container->get(InvoiceTemplateRepository::class), $container->get(InvoiceTemplateRepository::class), // @phpstan-ignore argument.type
$container->get(UserRepository::class), $container->get(UserRepository::class), // @phpstan-ignore argument.type
$container->get('event_dispatcher') $container->get('event_dispatcher') // @phpstan-ignore argument.type
)); ));
} }

View File

@@ -13,6 +13,7 @@ use App\Command\PromoteUserCommand;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
@@ -35,6 +36,7 @@ class PromoteUserCommandTest extends KernelTestCase
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
$userService = $container->get(UserService::class); $userService = $container->get(UserService::class);
$this->assertInstanceOf(UserService::class, $userService);
$this->application->add(new PromoteUserCommand($userService)); $this->application->add(new PromoteUserCommand($userService));
} }
@@ -80,8 +82,10 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] Role "ROLE_TEAMLEAD" has been added to user "john_user".', $output); $this->assertStringContainsString('[OK] Role "ROLE_TEAMLEAD" has been added to user "john_user".', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user'); $user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertTrue($user->hasTeamleadRole()); self::assertTrue($user->hasTeamleadRole());
@@ -95,8 +99,10 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] User "john_user" has been promoted as a super administrator.', $output); $this->assertStringContainsString('[OK] User "john_user" has been promoted as a super administrator.', $output);
$container = self::$kernel->getContainer(); $container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */ /** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class); $userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user'); $user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user); self::assertInstanceOf(User::class, $user);
self::assertTrue($user->isSuperAdmin()); self::assertTrue($user->isSuperAdmin());

View File

@@ -42,7 +42,7 @@ class VersionCommandTest extends KernelTestCase
$this->assertEquals($result . PHP_EOL, $output); $this->assertEquals($result . PHP_EOL, $output);
} }
public function getTestData(): array // @phpstan-ignore-line public function getTestData(): array // @phpstan-ignore missingType.iterableValue
{ {
return [ return [
[[], 'Kimai ' . Constants::VERSION . ' by Kevin Papst.'], [[], 'Kimai ' . Constants::VERSION . ' by Kevin Papst.'],

View File

@@ -20,6 +20,7 @@ use App\Tests\DataFixtures\TimesheetFixtures;
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock; use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Symfony\Component\DomCrawler\Field\ChoiceFormField; use Symfony\Component\DomCrawler\Field\ChoiceFormField;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\HttpKernelBrowser;
/** /**
@@ -203,7 +204,6 @@ class ActivityControllerTest extends ControllerBaseTest
]); ]);
$location = $this->assertIsModalRedirect($client, '/details'); $location = $this->assertIsModalRedirect($client, '/details');
self::assertNotNull($location);
$this->requestPure($client, $location); $this->requestPure($client, $location);
$this->assertDetailsPage($client); $this->assertDetailsPage($client);
@@ -221,7 +221,9 @@ class ActivityControllerTest extends ControllerBaseTest
public function testCreateActionShowsMetaFields(): void public function testCreateActionShowsMetaFields(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
self::getContainer()->get('event_dispatcher')->addSubscriber(new ActivityTestMetaFieldSubscriberMock()); $eventDispatcher = self::getContainer()->get('event_dispatcher');
static::assertInstanceOf(EventDispatcher::class, $eventDispatcher);
$eventDispatcher->addSubscriber(new ActivityTestMetaFieldSubscriberMock());
$this->assertAccessIsGranted($client, '/admin/activity/create'); $this->assertAccessIsGranted($client, '/admin/activity/create');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());

View File

@@ -19,7 +19,6 @@ use App\Tests\Mocks\SystemConfigurationFactory;
use OneLogin\Saml2\Auth; use OneLogin\Saml2\Auth;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Http\SecurityRequestAttributes; use Symfony\Component\Security\Http\SecurityRequestAttributes;
@@ -105,7 +104,6 @@ class SamlControllerTest extends TestCase
$sut = new SamlController($factory, $this->getSamlConfiguration()); $sut = new SamlController($factory, $this->getSamlConfiguration());
$result = $sut->metadataAction(); $result = $sut->metadataAction();
self::assertInstanceOf(Response::class, $result);
self::assertEquals('xml', $result->headers->get('Content-Type')); self::assertEquals('xml', $result->headers->get('Content-Type'));
$expected = new \DOMDocument(); $expected = new \DOMDocument();

Some files were not shown because too many files have changed in this diff Show More