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

View File

@@ -5,111 +5,30 @@
* file that was distributed with this source code.
*/
body {
font-family: sans-serif;
font-size: 10pt;
margin: 4px;
padding: 0;
}
table, tr, td, th, p, h1, h2, h3, h4, h5 {
padding: 0;
margin: 0;
}
th {
border: none;
text-align: left;
}
td {
vertical-align: top;
}
p {
margin-bottom: 12px;
}
.text-small {
font-size: 7pt;
}
.padding-left {
padding-left: 15px;
}
.text-center {
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;
}
body { font-family: sans-serif; font-size: 10pt; margin: 4px; padding: 0; }
table, tr, td, th, p, h1, h2, h3, h4, h5 { padding: 0; margin: 0;}
th { border: none; text-align: left; }
td { vertical-align: top; }
p { margin-bottom: 12px; }
.text-small { font-size: 7pt; }
.padding-left { padding-left: 15px; }
.text-center { 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; }
table.addresses { margin-bottom: 30px; }
table.header { padding-bottom: 4px; border-bottom: 1px solid #999; }
table.header .title { font-weight: normal; font-size: 140%; }
table.footer { border-top: 1px solid #999; font-size: 80%; padding-top: 8px; line-height: 14px; }
table.footer td { vertical-align: top; }
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);
}
}
/* 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;
}
&.overlapping {
border-top: 2px solid rgba(214,57,57,.1);
border-top: 2px solid rgba(214,57,57,.2);
}
&.exported {
opacity: 0.7;

View File

@@ -61,6 +61,7 @@
"symfony/mailer": "^6.0",
"symfony/mime": "^6.0",
"symfony/monolog-bundle": "^3.4",
"symfony/process": "^6.0",
"symfony/rate-limiter": "^6.0",
"symfony/runtime": "^6.0",
"symfony/security-bundle": "^6.0",
@@ -83,12 +84,12 @@
"doctrine/doctrine-fixtures-bundle": "^3.2",
"fakerphp/faker": "^1.15",
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpstan/phpstan-symfony": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
"phpunit/phpunit": "9.5.*",
"symfony/browser-kit": "^6.0",
"symfony/css-selector": "^6.0",
@@ -134,6 +135,10 @@
"symfony/polyfill-ctype": "*",
"symfony/polyfill-mbstring": "*",
"symfony/polyfill-intl": "*",
"symfony/polyfill-intl-icu": "*",
"symfony/polyfill-intl-grapheme": "*",
"symfony/polyfill-intl-idn": "*",
"symfony/polyfill-intl-normalizer": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php81": "*",
"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",
"devDependencies": {
"@babel/core": "^7.19",
"@babel/core": "^7.26",
"@babel/eslint-parser": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-env": "^7.26",
"@fortawesome/fontawesome-free": "^6.5",
"@fullcalendar/bootstrap5": "^5.11",
"@fullcalendar/core": "^5.11",
@@ -47,7 +47,10 @@
"sass": "^1.62",
"sass-loader": "^13.2",
"tom-select": "^2.2",
"webpack": "^5.91",
"webpack": "^5.96",
"webpack-cli": "^4.10"
},
"dependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.8.3"
}
}

View File

@@ -21,12 +21,10 @@ parameters:
booleansInConditions: false
uselessCast: true
requireParentConstructorCall: true
disallowedConstructs: false
overwriteVariablesWithLoop: true
closureUsesThis: true
matchingInheritedMethodNames: true
numericOperandsInArithmeticOperators: true
strictCalls: false
switchConditionsMatchingType: true
noVariableVariables: false
paths:
@@ -290,7 +288,7 @@ parameters:
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
path: src/Command/ExportCreateCommand.php
@@ -315,7 +313,7 @@ parameters:
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
path: src/Command/ExportCreateCommand.php
@@ -465,7 +463,7 @@ parameters:
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
path: src/Command/TranslationCommand.php
@@ -1294,16 +1292,6 @@ parameters:
count: 1
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$#"
count: 1
@@ -1465,7 +1453,7 @@ parameters:
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
path: src/Entity/UserPreference.php
@@ -1479,11 +1467,6 @@ parameters:
count: 1
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\\.$#"
count: 1
@@ -1809,11 +1792,6 @@ parameters:
count: 1
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\\.$#"
count: 1
@@ -2005,7 +1983,7 @@ parameters:
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
path: src/Form/Helper/ActivityHelper.php
@@ -2020,7 +1998,7 @@ parameters:
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
path: src/Form/Helper/CustomerHelper.php
@@ -2035,7 +2013,7 @@ parameters:
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
path: src/Form/Helper/ProjectHelper.php
@@ -3365,7 +3343,7 @@ parameters:
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
path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
@@ -3395,7 +3373,7 @@ parameters:
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
path: src/Invoice/Renderer/DocxRenderer.php
@@ -3559,11 +3537,6 @@ parameters:
count: 1
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\\.$#"
count: 1
@@ -3579,11 +3552,6 @@ parameters:
count: 1
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\\.$#"
count: 1
@@ -4450,7 +4418,7 @@ parameters:
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
path: src/Utils/LocaleFormatter.php
@@ -4460,17 +4428,17 @@ parameters:
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
path: src/Utils/LocaleFormatter.php
-
message: "#^Parameter \\#1 \\$num of method Symfony\\\\Polyfill\\\\Intl\\\\Icu\\\\NumberFormatter\\:\\:format\\(\\) expects float\\|int, float\\|int\\|string 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\\.$#"
message: "#^Parameter \\#2 \\$currency of method NumberFormatter\\:\\:formatCurrency\\(\\) expects string, string\\|null given\\.$#"
count: 1
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": {
"js": [
"/build/runtime.74179306.js",
"/build/app.6cdb49cb.js"
"/build/app.06423747.js"
],
"css": [
"/build/app.1ed18844.css"
"/build/app.6bd98062.css"
]
},
"app-rtl": {
@@ -15,7 +15,7 @@
"/build/app-rtl.97153087.js"
],
"css": [
"/build/app-rtl.d38ad12c.css"
"/build/app-rtl.6a14a027.css"
]
},
"export-pdf": {
@@ -72,10 +72,10 @@
},
"integrity": {
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
"/build/app.6cdb49cb.js": "sha384-psXCV8SG68O80ZJHHeSbCJPQ8RoepaMeI1pEypTHu0Q8ySLrhlclhKHIL2dZ0ZQJ",
"/build/app.1ed18844.css": "sha384-fEjS/tjc8m7jrw28/eWZFOTO6bugR9ILfksKvBrF46wtmqlage1s/P4+ZeYuqgpq",
"/build/app.06423747.js": "sha384-9bVxz1eOxdhs+XNwJ6Ug4/crO0CrQlWs1FDYQ1xF14LpFIZjuVO65YohHT3KWreU",
"/build/app.6bd98062.css": "sha384-BB9TrvQK/jlfhbdISEdBH5ZaZvbrPnZ+AcocXs1uQNh2yIQFboslUY9Up3iqnY4h",
"/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.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",

View File

@@ -1,7 +1,7 @@
{
"build/app.css": "/build/app.1ed18844.css",
"build/app.js": "/build/app.6cdb49cb.js",
"build/app-rtl.css": "/build/app-rtl.d38ad12c.css",
"build/app.css": "/build/app.6bd98062.css",
"build/app.js": "/build/app.06423747.js",
"build/app-rtl.css": "/build/app-rtl.6a14a027.css",
"build/app-rtl.js": "/build/app-rtl.97153087.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"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';
// TODO remove once PARTIAL usage was replaced entirely
\Doctrine\Deprecations\Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/8471');
return function (array $context) {
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 FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

View File

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

View File

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

View File

@@ -15,11 +15,11 @@ use App\Plugin\Plugin;
use App\Plugin\PluginManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\PhpSubprocess;
#[AsCommand(name: 'kimai:plugins', description: 'Manage Kimai plugins')]
final class PluginCommand extends Command
@@ -69,14 +69,28 @@ final class PluginCommand extends Command
}
}
$command = $this->getApplication()?->find('doctrine:migrations:migrate');
if ($command === null) {
throw new \RuntimeException('Failed finding doctrine migrations command');
}
$cmdInput = new ArrayInput(['--allow-no-migration' => true, '--configuration' => $config]);
$cmdInput->setInteractive(false);
if (0 !== $command->run($cmdInput, $output)) {
$io->error('Failed to install bundle database: ' . $config);
// using getApplication()->find('doctrine:migrations:migrate') does NOT work here
// because the Doctrine command can only be executed once
// if run more than once it fails with a "Container is frozen" exception
$process = new PhpSubprocess([
'bin/console',
'doctrine:migrations:migrate',
'--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);
$settings['date'] = $shortDate->getPattern();
if ($settings['date'] === false) {
$io->error('Invalid date pattern for locale: ' . $locale);
continue;
}
$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
// 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('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-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('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
->addOption('target', null, InputOption::VALUE_REQUIRED, 'Single target file to use')
// DEEPL TRANSLATION FEATURE - UNTESTED
->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale with Deepl')
@@ -68,10 +70,13 @@ final class TranslationCommand extends Command
$bases = [
'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 = [];
if ($input->getOption('source') !== null) {
/** @var string $tmp */
@@ -146,6 +151,19 @@ final class TranslationCommand extends Command
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
// ==========================================================================
@@ -634,4 +652,66 @@ final class TranslationCommand extends Command
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
*/
public const VERSION = '2.24.0';
public const VERSION = '2.25.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 22400;
public const VERSION_ID = 22500;
/**
* The software name
*/

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ final class UTCDateTimeType extends DateTimeType
* @param T $value
* @param AbstractPlatform $platform
* @return (T is null ? null : string)
* @template T<\DateTime>
* @template T
* @throws ConversionException
*/
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
// those cannot be searched in the database
if (null !== $value) {
switch ($this->type) {
case YesNoType::class:
case CheckboxType::class:
if (!\is_int($value) && !\is_bool($value) && !\is_string($value)) {
throw new \InvalidArgumentException('Failed converting meta-field bool value');
} else {
$value = (string) $value;
}
}
switch ($this->type) {
case YesNoType::class:
case CheckboxType::class:
if ($value === false || $value === '' || !\is_scalar($value)) {
$value = 0;
} else {
$value = 1;
}
}
if ($value === null) {

View File

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

View File

@@ -624,11 +624,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
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);
foreach ($values as $k => $v) {
@@ -639,7 +637,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/** @var TimesheetMeta $meta */
foreach ($this->meta as $meta) {
$timesheet->setMetaField(clone $meta);
$tmp = clone $meta;
$tmp->setEntity($timesheet);
$timesheet->setMetaField($tmp);
}
$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.
*
* @internal immutable property that cannot be set by plugins
* @param bool $canSeeAllData
* @return bool
* @throws Exception
*/
public function initCanSeeAllData(bool $canSeeAllData): bool
{

View File

@@ -150,11 +150,26 @@ class UserPreference
* integer, float, string, boolean or null
*
* @param mixed $value
* @return 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;
}

View File

@@ -157,7 +157,7 @@ class PageActionsEvent extends ThemeEvent
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
@@ -172,7 +172,7 @@ class PageActionsEvent extends ThemeEvent
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
{
$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
{
$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
{
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 {
$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;
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)) {
$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)) {
@@ -38,7 +38,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('duplicate', $timesheet)) {
$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) {
@@ -49,7 +49,6 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
$event->addAction('trash', [
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
'class' => 'api-link text-red dd-ts-trash',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.timesheetDelete',
'data-method' => 'DELETE',

View File

@@ -31,7 +31,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
}
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)) {
@@ -40,7 +40,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $activity)) {
$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) {
@@ -53,7 +53,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
$parameters['customers[]'] = $activity->getProject()->getCustomer()->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')) {
@@ -65,7 +65,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
if (!$activity->isGlobal()) {
$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)) {

View File

@@ -34,7 +34,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
$isListingView = $event->isIndexView() || $event->isCustomView();
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)) {
@@ -43,7 +43,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $customer)) {
$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) {
@@ -61,15 +61,15 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
}
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')) {
$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')) {
$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')) {

View File

@@ -56,7 +56,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
$allowDelete = $this->isGranted('delete_invoice');
if (!$invoice->isCanceled()) {
$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')) {

View File

@@ -34,7 +34,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
$isListingView = $event->isIndexView() || $event->isCustomView();
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)) {
@@ -43,7 +43,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('permissions', $project)) {
$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) {
@@ -51,11 +51,11 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
}
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')) {
$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')) {
@@ -79,7 +79,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
if (\array_key_exists('token', $payload) && $this->isGranted('edit', $project) && $this->isGranted('create_project')) {
$event->addAction(
'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')) {
$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')) {
$event->addAction('trash', [
'url' => $this->path('delete_tag', ['id' => $id]),
'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.tagDelete kimai.tagUpdate',
'data-method' => 'DELETE',

View File

@@ -36,7 +36,7 @@ final class TeamSubscriber extends AbstractActionsSubscriber
}
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', [
'url' => $this->path('delete_team', ['id' => $team->getId()]),
'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.teamDelete kimai.teamUpdate',
'data-method' => 'DELETE',

View File

@@ -22,7 +22,7 @@ final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
{
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('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')) {

View File

@@ -31,7 +31,7 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
}
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)) {
$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)) {
$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();
}
@@ -62,7 +62,7 @@ final class UserSubscriber extends AbstractActionsSubscriber
}
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')) {

View File

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

View File

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

View File

@@ -38,6 +38,8 @@ final class ActivityType extends AbstractType
public function groupBy(Activity $activity, $key, $index): string
{
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 '';
}

View File

@@ -13,6 +13,7 @@ use App\Configuration\MailConfiguration;
use App\Entity\User;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\RawMessage;
@@ -31,7 +32,11 @@ final class KimaiMailer implements MailerInterface
}
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);

View File

@@ -17,7 +17,7 @@ use DateTimeInterface;
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 DateTimeInterface $begin;
@@ -50,7 +50,7 @@ final class MonthlyStatistic implements DateStatisticInterface
$day = (int) $begin->format('d');
while ($tmp < $this->end) {
$curYear = $tmp->format('Y');
$curYear = (string) $tmp->format('Y');
if (!isset($years[$curYear])) {
$year = [];
for ($i = 1; $i < 13; $i++) {
@@ -67,7 +67,7 @@ final class MonthlyStatistic implements DateStatisticInterface
}
$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\ActivityRepository;
use App\Repository\ProjectRateRepository;
use App\Repository\Query\ActivityQuery;
final class ProjectDuplicationService
{
@@ -53,7 +54,11 @@ final class ProjectDuplicationService
$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) {
$newActivity = clone $activity;
$newActivity->setProject($newProject);

View File

@@ -37,18 +37,6 @@ class ActivityRepository extends EntityRepository
{
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
* @return array<Activity>

View File

@@ -64,7 +64,7 @@ trait RepositorySearchTrait
$c = 0;
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
$and = $qb->expr()->andX();
/** @var non-falsy-string&literal-string $alias */
/** @var non-falsy-string&lowercase-string $alias */
$alias = 'meta' . $a++;
$paramName = 'metaName' . $i++;
$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 InvalidArgumentException for already persisted timesheets
* @throws AccessDeniedException if user is not allowed to start timesheet
@@ -233,11 +231,10 @@ final class TimesheetService
}
/**
* @param Timesheet $timesheet
* @param string[] $groups
* @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);

View File

@@ -19,6 +19,7 @@ use App\Utils\LocaleFormatter;
use DateTime;
use DateTimeInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -41,7 +42,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
new TwigFilter('date_short', [$this, 'dateShort']),
new TwigFilter('date_time', [$this, 'dateTime']),
// 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_weekday', [$this, 'dateWeekday']),
new TwigFilter('time', [$this, 'time']),

View File

@@ -31,7 +31,7 @@ final class LocaleFormatter
private ?NumberFormatter $moneyFormatter = 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\Entity\Timesheet as TimesheetEntity;
use App\Form\Model\MultiUserTimesheet;
use App\Repository\TimesheetRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@@ -35,6 +36,10 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
if ($value instanceof MultiUserTimesheet) {
return;
}
$begin = $value->getBegin();
$end = $value->getEnd();

View File

@@ -28,13 +28,13 @@ final class WorkingTimeCalculatorDay implements WorkingTimeCalculator
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
{
return (int) match ($dateTime->format('N')) {
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0),
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0),
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0),
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0),
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0),
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0),
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0),
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0, false),
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0, false),
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0, false),
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0, false),
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0, false),
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0, false),
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0, false),
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
{
public function __construct(\DateTimeImmutable $month, private User $user)
public function __construct(\DateTimeImmutable $month, private readonly User $user)
{
parent::__construct($month);
}

View File

@@ -503,18 +503,6 @@
"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": {
"version": "v4.0.3"
},

View File

@@ -3,7 +3,7 @@
{% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% 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,
'back': path('admin_activity')
} %}

View File

@@ -2,7 +2,7 @@
{% block main %}
{{ 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,
'back': path('activity_details', {'id': activity.id})
}) }}

View File

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

View File

@@ -2,7 +2,7 @@
{% block main %}
{{ 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,
'back': path('customer_details', {'id': customer.id})
}) }}

View File

@@ -49,9 +49,9 @@
<h3 class="card-title">{{ widget.title|trans({}, widget.translationDomain) }}</h3>
<div class="card-actions">
{% 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 %}
{{ 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 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" />
{% endblock %}
{% 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 %}
{% if _reset is not same as (false) %}
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />

View File

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

View File

@@ -19,7 +19,7 @@
{% endblock %}
{% block box_attributes %}id="team_listing_box"{% endblock %}
{% 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 %}
{% 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">
<div class="card mb-1 p-0">
<div class="card-header">
{{ 'settings'|trans({}, 'actions') }}
{{ 'settings'|trans }}
</div>
<div class="card-body">
<label for="duration-decimal">

View File

@@ -27,7 +27,7 @@
{% endblock %}
{% block box_footer %}
<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 %}
{% endembed %}
{% elseif upload_error is not null %}

View File

@@ -224,7 +224,7 @@
{% else %}
{% 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 %}
{{ headerTitle }}
{% 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 %}
<div class="card-footer d-flex align-items-center">
{% 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 %}
{{ form_widget(formChild) }}
{% endfor %}

View File

@@ -63,7 +63,7 @@
{% if values.children is defined and values.children|length > 0 %}
<div class="dropdown">
<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>
<div class="dropdown-menu">
{%- for icon, values in values.children %}

View File

@@ -3,7 +3,7 @@
{% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% 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,
'back': path('admin_project')
} %}

View File

@@ -2,7 +2,7 @@
{% block main %}
{{ 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,
'back': path('project_details', {'id': project.id})
}) }}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
{% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% 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,
'back': path('user_profile_access_token', {'username': user.userIdentifier})
} %}

View File

@@ -225,7 +225,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
if (\count($globalError) > 0) {
self::assertArrayHasKey('errors', $result['errors']);
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);
foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item);
}
$i = 0;
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));
$i++;
}
@@ -110,11 +112,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item);
}
$i = 0;
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));
$i++;
}
@@ -159,11 +163,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item);
}
$i = 0;
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));
$i++;
}
@@ -204,11 +210,13 @@ class ActionsControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
foreach ($result as $item) {
self::assertIsArray($item);
self::assertApiResponseTypeStructure('PageActionItem', $item);
}
$i = 0;
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));
$i++;
}

View File

@@ -21,6 +21,7 @@ use App\Repository\ActivityRateRepository;
use App\Repository\ActivityRepository;
use App\Repository\Query\VisibilityInterface;
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* @group integration
@@ -179,6 +180,7 @@ class ActivityControllerTest extends APIControllerBaseTest
for ($i = 0; $i < \count($result); $i++) {
$activity = $result[$i];
$hasProject = $expected[$i][0];
self::assertIsArray($activity);
self::assertApiResponseTypeStructure('ActivityCollection', $activity);
if ($hasProject && $projectId !== null) {
$this->assertEquals($projectId, $activity['project']);
@@ -219,6 +221,7 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(5, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('ActivityCollection', $result[0]);
$this->assertEquals($imports[0]->getId(), $result[4]['project']);
$this->assertEquals($imports[1]->getId(), $result[3]['project']);
@@ -429,7 +432,9 @@ class ActivityControllerTest extends APIControllerBaseTest
public function testMetaAction(): void
{
$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 = [
'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\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
/**
* @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']);
$passport = $sut->authenticate($request);
self::assertInstanceOf(Passport::class, $passport);
$badge = $passport->getBadge(UserBadge::class);
self::assertInstanceOf(UserBadge::class, $badge);
self::assertEquals('foo2', $badge->getUserIdentifier());

View File

@@ -21,6 +21,7 @@ use App\Entity\User;
use App\Repository\CustomerRateRepository;
use App\Repository\CustomerRepository;
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* @group integration
@@ -104,6 +105,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(1, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
}
@@ -120,6 +122,7 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(1, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
}
@@ -372,7 +375,9 @@ class CustomerControllerTest extends APIControllerBaseTest
public function testMetaAction(): void
{
$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 = [
'name' => 'metatestmock',

View File

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

View File

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

View File

@@ -150,7 +150,13 @@ trait RateControllerTestTrait
$this->assertEquals(\count($expectedRates), \count($result));
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->assertNotEmpty($result);
$this->assertEquals(3, \count($result));
self::assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TagEntity', $result[0]);
}

View File

@@ -65,6 +65,7 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
$this->assertNotEmpty($result);
self::assertEquals(2, \count($result));
self::assertIsArray($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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$updateId = $result['id'];
$this->assertIsNumeric($updateId);
$data = [
'name' => 'foo',
@@ -171,14 +174,17 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']);
self::assertCount(3, $result['members']);
$this->assertIsNumeric($updateId);
$this->request($client, '/api/teams/' . $updateId);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['members']);
self::assertCount(3, $result['members']);
$this->assertIsArray($result['members'][1]);
self::assertFalse($result['members'][1]['teamlead']);
self::assertEquals(1, $result['members'][1]['user']['id']);
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('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('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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$data = [
'name' => '1',
@@ -230,6 +238,7 @@ class TeamControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertNotEmpty($result['id']);
$id = $result['id'];
$this->assertIsNumeric($id);
$this->request($client, '/api/teams/' . $id, 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -249,7 +258,10 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['members']);
self::assertCount(1, $result['members']);
$this->assertIsNumeric($result['id']);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -257,6 +269,7 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$this->assertIsArray($result['members']);
self::assertCount(4, $result['members']);
$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);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['customers']);
$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);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['customers']);
self::assertCount(1, $result['customers']);
$this->assertIsArray($result['customers'][0]);
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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['customers']);
// cannot add existing customer
@@ -416,12 +445,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['customers']);
// add customer
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['customers']);
$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);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['customers']);
self::assertCount(0, $result['customers']);
/** @var EntityManager $em */
@@ -454,6 +490,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['projects']);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -484,7 +524,9 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['projects']);
self::assertCount(1, $result['projects']);
$this->assertIsArray($result['projects'][0]);
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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['projects']);
// cannot add existing project
@@ -535,12 +581,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['projects']);
// add project
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['projects']);
self::assertCount(1, $result['projects']);
$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);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['projects']);
self::assertCount(0, $result['projects']);
/** @var EntityManager $em */
@@ -573,6 +624,8 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(0, $result['activities']);
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -603,7 +658,9 @@ class TeamControllerTest extends APIControllerBaseTest
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($result['activities']);
self::assertCount(1, $result['activities']);
$this->assertIsArray($result['activities'][0]);
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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['activities']);
// cannot add existing activity
@@ -652,12 +713,16 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsArray($result['activities']);
self::assertCount(0, $result['activities']);
// add activity
$this->request($client, '/api/teams/' . $result['id'] . '/activities/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
self::assertCount(1, $result['activities']);
$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);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TeamEntity', $result);
$this->assertIsArray($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->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertIsNumeric($result['id']);
// team not found
$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\Mocks\TimesheetTestMetaFieldSubscriberMock;
use App\Timesheet\DateTimeFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Response;
/**
@@ -63,6 +64,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -79,6 +81,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
@@ -103,6 +106,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['users' => [2]];
@@ -115,6 +119,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -162,6 +167,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -186,6 +192,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(17, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -237,6 +244,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(4, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -289,6 +297,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(5, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -324,6 +333,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(7, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [
@@ -342,6 +352,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [
@@ -358,6 +369,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(17, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -896,6 +908,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
self::assertNotEmpty($result['id']);
self::assertIsNumeric($result['id']);
$id = $result['id'];
$this->request($client, '/api/timesheets/' . $id, 'DELETE');
@@ -999,6 +1012,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(1, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
@@ -1024,6 +1038,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertEquals(3, \count($result));
foreach ($result as $timesheet) {
$this->assertIsArray($timesheet);
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet);
}
}
@@ -1142,6 +1157,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Test', 'Admin']];
@@ -1154,6 +1170,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Nothing-2-see', 'here']];
@@ -1166,6 +1183,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(20, \count($result));
$this->assertIsArray($result[0]);
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
@@ -1190,6 +1208,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']);
@@ -1227,6 +1246,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']);
@@ -1272,6 +1292,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
self::assertEquals('foo', $result['description']);
self::assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
@@ -1342,8 +1363,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
self::assertEquals(2016, $result['rate']);
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH');
$id = $result['id'];
self::assertIsNumeric($id);
$this->request($client, '/api/timesheets/' . $id . '/duplicate', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
@@ -1380,6 +1402,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em->clear();
@@ -1456,7 +1479,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
$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 = [
'name' => 'metatestmock',
@@ -1472,6 +1497,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
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->assertEquals(7, \count($result));
foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user);
}
}
@@ -70,6 +71,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result);
$this->assertEquals(7, \count($result));
foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserEntity', $user);
}
}
@@ -86,6 +88,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result);
$this->assertEquals(1, \count($result));
foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user);
}
}
@@ -102,6 +105,7 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertNotEmpty($result);
$this->assertEquals(8, \count($result));
foreach ($result as $user) {
$this->assertIsArray($user);
self::assertApiResponseTypeStructure('UserCollection', $user);
}
}
@@ -283,7 +287,9 @@ class UserControllerTest extends APIControllerBaseTest
'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());
$content = $client->getResponse()->getContent();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ use App\Command\DemoteUserCommand;
use App\Entity\User;
use App\Repository\UserRepository;
use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException;
@@ -34,6 +35,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->application = new Application($kernel);
$container = self::$kernel->getContainer();
/** @var UserService $userService */
$userService = $container->get(UserService::class);
$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);
$container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class);
$userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('tony_teamlead');
self::assertInstanceOf(User::class, $user);
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);
$container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class);
$userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('susan_super');
self::assertInstanceOf(User::class, $user);
self::assertFalse($user->isSuperAdmin());

View File

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

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Command;
use App\Command\InstallCommand;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -27,9 +28,11 @@ class InstallCommandTest extends KernelTestCase
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
$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)) {
$files = glob($path . '*');
if ($files === false) {
return;
}
foreach ($files as $file) {
unlink($file);
}
@@ -64,12 +67,12 @@ class InvoiceCreateCommandTest extends KernelTestCase
$container = self::getContainer();
$this->application->add(new InvoiceCreateCommand(
$container->get(ServiceInvoice::class),
$container->get(CustomerRepository::class),
$container->get(ProjectRepository::class),
$container->get(InvoiceTemplateRepository::class),
$container->get(UserRepository::class),
$container->get('event_dispatcher')
$container->get(ServiceInvoice::class), // @phpstan-ignore argument.type
$container->get(CustomerRepository::class), // @phpstan-ignore argument.type
$container->get(ProjectRepository::class), // @phpstan-ignore argument.type
$container->get(InvoiceTemplateRepository::class), // @phpstan-ignore argument.type
$container->get(UserRepository::class), // @phpstan-ignore argument.type
$container->get('event_dispatcher') // @phpstan-ignore argument.type
));
}

View File

@@ -13,6 +13,7 @@ use App\Command\PromoteUserCommand;
use App\Entity\User;
use App\Repository\UserRepository;
use App\User\UserService;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Exception\RuntimeException;
@@ -35,6 +36,7 @@ class PromoteUserCommandTest extends KernelTestCase
$container = self::$kernel->getContainer();
$userService = $container->get(UserService::class);
$this->assertInstanceOf(UserService::class, $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);
$container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class);
$userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user);
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);
$container = self::$kernel->getContainer();
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
/** @var UserRepository $userRepository */
$userRepository = $container->get('doctrine')->getRepository(User::class);
$userRepository = $doctrine->getRepository(User::class);
$user = $userRepository->loadUserByIdentifier('john_user');
self::assertInstanceOf(User::class, $user);
self::assertTrue($user->isSuperAdmin());

View File

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

View File

@@ -20,6 +20,7 @@ use App\Tests\DataFixtures\TimesheetFixtures;
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\HttpKernelBrowser;
/**
@@ -203,7 +204,6 @@ class ActivityControllerTest extends ControllerBaseTest
]);
$location = $this->assertIsModalRedirect($client, '/details');
self::assertNotNull($location);
$this->requestPure($client, $location);
$this->assertDetailsPage($client);
@@ -221,7 +221,9 @@ class ActivityControllerTest extends ControllerBaseTest
public function testCreateActionShowsMetaFields(): void
{
$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->assertTrue($client->getResponse()->isSuccessful());

View File

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

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