Release 2.21.0 (#5014)

This commit is contained in:
Kevin Papst
2024-08-28 17:16:37 +02:00
committed by GitHub
parent 75fa7a20b8
commit b1903ba183
54 changed files with 284 additions and 430 deletions

View File

@@ -85,7 +85,7 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable')); kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action')); kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link')); kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form')); kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form', ['td.multiCheckbox', 'td.actions']));
kimai.registerPlugin(new KimaiRemoteModal()); kimai.registerPlugin(new KimaiRemoteModal());
kimai.registerPlugin(new KimaiActiveRecords()); kimai.registerPlugin(new KimaiActiveRecords());
kimai.registerPlugin(new KimaiAPILink('api-link')); kimai.registerPlugin(new KimaiAPILink('api-link'));

View File

@@ -17,9 +17,10 @@ import { Modal } from 'bootstrap';
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler { export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
constructor(selector) { constructor(selector, stopSelector) {
super(); super();
this._selector = selector; this._selector = selector;
this._stopSelector = stopSelector;
} }
getId() getId()
@@ -63,7 +64,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
this.addClickHandler(this._selector, (href) => { this.addClickHandler(this._selector, (href) => {
this.openUrlInModal(href); this.openUrlInModal(href);
}); }, this._stopSelector);
} }
_getModal() _getModal()

View File

@@ -24,7 +24,7 @@ export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
init() { init() {
this.addClickHandler(this._selector, function(href) { this.addClickHandler(this._selector, function(href) {
window.location = href; window.location = href;
}); }, []);
} }
} }

View File

@@ -15,10 +15,11 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
/** /**
* No _underscore naming for now, as it would be mangled otherwise * No _underscore naming for now, as it would be mangled otherwise
* @param selector * @param {string} selector
* @param callback * @param {callback} callback
* @param {array<string>} stopSelector
*/ */
addClickHandler(selector, callback) { addClickHandler(selector, callback, stopSelector) {
document.body.addEventListener('click', (event) => { document.body.addEventListener('click', (event) => {
// event.currentTarget is ALWAYS the body // event.currentTarget is ALWAYS the body
@@ -38,6 +39,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
return; return;
} }
for (let x of stopSelector) {
if (target.matches(x)) {
return;
}
}
target = target.parentNode; target = target.parentNode;
} }
@@ -54,6 +61,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
return; return;
} }
for (let x of stopSelector) {
if (target.matches(x)) {
return;
}
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

34
composer.lock generated
View File

@@ -10448,16 +10448,16 @@
}, },
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.3.0", "version": "3.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81" "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81", "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81", "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10507,7 +10507,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.0" "source": "https://github.com/composer/pcre/tree/3.3.1"
}, },
"funding": [ "funding": [
{ {
@@ -10523,7 +10523,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-08-19T19:43:53+00:00" "time": "2024-08-27T18:44:43+00:00"
}, },
{ {
"name": "composer/xdebug-handler", "name": "composer/xdebug-handler",
@@ -11002,16 +11002,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.62.0", "version": "v3.63.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "627692f794d35c43483f34b01d94740df2a73507" "reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/627692f794d35c43483f34b01d94740df2a73507", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9d427f3f14984403a6ae9fc726b61765ca0c005e",
"reference": "627692f794d35c43483f34b01d94740df2a73507", "reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11093,7 +11093,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.62.0" "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.63.2"
}, },
"funding": [ "funding": [
{ {
@@ -11101,7 +11101,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-08-07T17:03:09+00:00" "time": "2024-08-28T10:47:21+00:00"
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
@@ -11348,16 +11348,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.11.11", "version": "1.12.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3" "reference": "384af967d35b2162f69526c7276acadce534d0e1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1",
"reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3", "reference": "384af967d35b2162f69526c7276acadce534d0e1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11402,7 +11402,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-08-19T14:37:29+00:00" "time": "2024-08-27T09:18:05+00:00"
}, },
{ {
"name": "phpstan/phpstan-deprecation-rules", "name": "phpstan/phpstan-deprecation-rules",

View File

@@ -7,6 +7,8 @@ framework:
csrf_protection: true csrf_protection: true
annotations: false annotations: false
handle_all_throwables: true handle_all_throwables: true
disallow_search_engine_index: true
set_content_language_from_locale: true
serializer: serializer:
enable_attributes: true enable_attributes: true

View File

@@ -23,7 +23,3 @@ services:
App\User\UserService: App\User\UserService:
public: true public: true
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher'] arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']
App\Plugin\PluginManager:
public: true
arguments: [!tagged kimai.plugin]

View File

@@ -34,6 +34,7 @@ parameters:
tmpDir: %rootDir%/../../../var/cache/phpstan tmpDir: %rootDir%/../../../var/cache/phpstan
excludePaths: excludePaths:
- %rootDir%/../../../src/Ldap/LdapDriver.php - %rootDir%/../../../src/Ldap/LdapDriver.php
- %rootDir%/../../../src/Event/PageActionsEvent.php
treatPhpDocTypesAsCertain: false treatPhpDocTypesAsCertain: false
inferPrivatePropertyTypeFromConstructor: true inferPrivatePropertyTypeFromConstructor: true
doctrine: doctrine:
@@ -1368,11 +1369,6 @@ parameters:
count: 1 count: 1
path: src/Entity/Tag.php path: src/Entity/Tag.php
-
message: "#^Property App\\\\Entity\\\\Tag\\:\\:\\$timesheets with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
count: 1
path: src/Entity/Tag.php
- -
message: "#^Cannot call method removeMembership\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" message: "#^Cannot call method removeMembership\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1 count: 1
@@ -1613,26 +1609,6 @@ parameters:
count: 1 count: 1
path: src/Event/CalendarConfigurationEvent.php path: src/Event/CalendarConfigurationEvent.php
-
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:addAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/PageActionsEvent.php
-
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:addActionToSubmenu\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/PageActionsEvent.php
-
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:getActions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/PageActionsEvent.php
-
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:replaceAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/PageActionsEvent.php
- -
message: "#^Method App\\\\Event\\\\PermissionsEvent\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Event\\\\PermissionsEvent\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -2098,21 +2074,6 @@ parameters:
count: 1 count: 1
path: src/Form/CustomerEditForm.php path: src/Form/CustomerEditForm.php
-
message: "#^Class App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
count: 1
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
-
message: "#^Parameter \\#1 \\$formatToInt \\(string\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer\\:\\:reverseTransform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:reverseTransform\\(\\)$#"
count: 1
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
-
message: "#^Parameter \\#1 \\$intToFormat \\(int\\) of method App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer\\:\\:transform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:transform\\(\\)$#"
count: 1
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
- -
message: "#^Class App\\\\Form\\\\DataTransformer\\\\SearchTermTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#" message: "#^Class App\\\\Form\\\\DataTransformer\\\\SearchTermTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
count: 1 count: 1
@@ -3963,166 +3924,11 @@ parameters:
count: 1 count: 1
path: src/Reporting/ProjectDetails/ProjectDetailsModel.php path: src/Reporting/ProjectDetails/ProjectDetailsModel.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/ActivityRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/ActivityRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 2
path: src/Repository/ActivityRepository.php
-
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/ActivityRepository.php
-
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/ActivityRepository.php
-
message: "#^Method App\\\\Repository\\\\BookmarkRepository\\:\\:deleteBookmark\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/BookmarkRepository.php
-
message: "#^Method App\\\\Repository\\\\BookmarkRepository\\:\\:saveBookmark\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/BookmarkRepository.php
-
message: "#^Parameter \\#1 \\$string of function mb_substr expects string, string\\|null given\\.$#"
count: 1
path: src/Repository/BookmarkRepository.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Repository\\\\BookmarkRepository\\:\\:clearCache\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 2
path: src/Repository/BookmarkRepository.php
-
message: "#^Property App\\\\Repository\\\\BookmarkRepository\\:\\:\\$userCache type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/BookmarkRepository.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 2
path: src/Repository/CustomerRepository.php
-
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:getComments\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Cannot access offset 'counter' on mixed\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 2
path: src/Repository/InvoiceRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:addPermissionCriteria\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:getCounterFor\\(\\) should return int but returns mixed\\.$#"
count: 1
path: src/Repository/InvoiceRepository.php
- -
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1 count: 1
path: src/Repository/Loader/ProjectLoader.php path: src/Repository/Loader/ProjectLoader.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1
path: src/Repository/Loader/TimesheetLoader.php
-
message: "#^Interface App\\\\Repository\\\\Paginator\\\\PaginatorInterface extends generic interface Pagerfanta\\\\Adapter\\\\AdapterInterface but does not specify its types\\: T$#"
count: 1
path: src/Repository/Paginator/PaginatorInterface.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/ProjectRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/ProjectRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 2
path: src/Repository/ProjectRepository.php
-
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/ProjectRepository.php
-
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:getComments\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/ProjectRepository.php
-
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/ProjectRepository.php
- -
message: "#^Method App\\\\Repository\\\\Query\\\\BaseQuery\\:\\:getOrderGroups\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Repository\\\\Query\\\\BaseQuery\\:\\:getOrderGroups\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -4168,11 +3974,6 @@ parameters:
count: 1 count: 1
path: src/Repository/Query/TimesheetQuery.php path: src/Repository/Query/TimesheetQuery.php
-
message: "#^Method App\\\\Repository\\\\Result\\\\TimesheetResult\\:\\:toIterable\\(\\) return type has no value type specified in iterable type iterable\\.$#"
count: 1
path: src/Repository/Result/TimesheetResult.php
- -
message: "#^Method App\\\\Repository\\\\RoleRepository\\:\\:deleteRole\\(\\) has no return type specified\\.$#" message: "#^Method App\\\\Repository\\\\RoleRepository\\:\\:deleteRole\\(\\) has no return type specified\\.$#"
count: 1 count: 1
@@ -4183,16 +3984,6 @@ parameters:
count: 1 count: 1
path: src/Repository/RoleRepository.php path: src/Repository/RoleRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/TagRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/TagRepository.php
- -
message: "#^Argument of an invalid type array\\<string\\>\\|string supplied for foreach, only iterables are supported\\.$#" message: "#^Argument of an invalid type array\\<string\\>\\|string supplied for foreach, only iterables are supported\\.$#"
count: 1 count: 1
@@ -4238,21 +4029,6 @@ parameters:
count: 1 count: 1
path: src/Repository/TimesheetRepository.php path: src/Repository/TimesheetRepository.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/TimesheetRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/TimesheetRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 2
path: src/Repository/TimesheetRepository.php
- -
message: "#^Cannot clone DateTime\\|null\\.$#" message: "#^Cannot clone DateTime\\|null\\.$#"
count: 1 count: 1
@@ -4303,31 +4079,6 @@ parameters:
count: 1 count: 1
path: src/Repository/TimesheetRepository.php path: src/Repository/TimesheetRepository.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/UserRepository.php
-
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/UserRepository.php
-
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1
path: src/Repository/UserRepository.php
-
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:findByUsername\\(\\) has parameter \\$username with no type specified\\.$#"
count: 1
path: src/Repository/UserRepository.php
-
message: "#^Parameter \\#1 \\$identifier of method App\\\\Repository\\\\UserRepository\\:\\:loadUserByIdentifier\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Repository/UserRepository.php
- -
message: "#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Security\\\\Core\\\\Exception\\\\AuthenticationException constructor expects string, string\\|null given\\.$#" message: "#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Security\\\\Core\\\\Exception\\\\AuthenticationException constructor expects string, string\\|null given\\.$#"
count: 1 count: 1

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,7 +3,7 @@
"app": { "app": {
"js": [ "js": [
"/build/runtime.74179306.js", "/build/runtime.74179306.js",
"/build/app.41121fe4.js" "/build/app.13d5a0d9.js"
], ],
"css": [ "css": [
"/build/app.a7ba9fd6.css" "/build/app.a7ba9fd6.css"
@@ -72,7 +72,7 @@
}, },
"integrity": { "integrity": {
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG", "/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
"/build/app.41121fe4.js": "sha384-4v3lVToJIcpMJ++4WaIZ5r9rXR5Vb4waSoHcEzx3vOfcLL+1+TN70di7XPzYSW7I", "/build/app.13d5a0d9.js": "sha384-1g1GY1GLRW3qbMZV2ZywyP9MR1Z8N65ayJqb+hyV2xyrhzY3JoVPyFF9IWozaEK4",
"/build/app.a7ba9fd6.css": "sha384-GfMYf1vYI9zkkywMoxYk6wpC4UyK185zjDBi16qa0237oztrtOuWdBZsKjWOkyeY", "/build/app.a7ba9fd6.css": "sha384-GfMYf1vYI9zkkywMoxYk6wpC4UyK185zjDBi16qa0237oztrtOuWdBZsKjWOkyeY",
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr", "/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
"/build/app-rtl.6516df2d.css": "sha384-5hF5akg98Hnj/bihpbsiTjMyZ3BsAC4+Z1kzqi1O3/U+tgpnQ+JmwsOADDpMld6d", "/build/app-rtl.6516df2d.css": "sha384-5hF5akg98Hnj/bihpbsiTjMyZ3BsAC4+Z1kzqi1O3/U+tgpnQ+JmwsOADDpMld6d",

View File

@@ -1,6 +1,6 @@
{ {
"build/app.css": "/build/app.a7ba9fd6.css", "build/app.css": "/build/app.a7ba9fd6.css",
"build/app.js": "/build/app.41121fe4.js", "build/app.js": "/build/app.13d5a0d9.js",
"build/app-rtl.css": "/build/app-rtl.6516df2d.css", "build/app-rtl.css": "/build/app-rtl.6516df2d.css",
"build/app-rtl.js": "/build/app-rtl.97153087.js", "build/app-rtl.js": "/build/app-rtl.97153087.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",

View File

@@ -46,6 +46,7 @@ final class TagController extends BaseApiController
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')] #[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]
public function cgetAction(ParamFetcherInterface $paramFetcher): Response public function cgetAction(ParamFetcherInterface $paramFetcher): Response
{ {
// not using a query, as these are too expensive to fetch, the simple "name" list is easier to use
$filter = $paramFetcher->get('name'); $filter = $paramFetcher->get('name');
$data = $this->repository->findAllTagNames($filter); $data = $this->repository->findAllTagNames($filter);
@@ -57,7 +58,7 @@ final class TagController extends BaseApiController
} }
/** /**
* Fetch tags by filter as entities * Fetch tags by filter (as full entities)
*/ */
#[OA\Response(response: 200, description: 'Find the collection of all matching tags', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TagEntity')))] #[OA\Response(response: 200, description: 'Find the collection of all matching tags', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TagEntity')))]
#[Route(path: '/find', name: 'get_tags_full', methods: ['GET'])] #[Route(path: '/find', name: 'get_tags_full', methods: ['GET'])]

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.20.0'; public const VERSION = '2.21.0';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 22000; public const VERSION_ID = 22100;
/** /**
* The software name * The software name
*/ */

View File

@@ -41,12 +41,14 @@ final class PermissionController extends AbstractController
{ {
public const TOKEN_NAME = 'user_role_permissions'; public const TOKEN_NAME = 'user_role_permissions';
public function __construct(private RolePermissionManager $manager, private RoleRepository $roleRepository) public function __construct(
private readonly RolePermissionManager $manager,
private readonly RoleRepository $roleRepository
)
{ {
} }
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])] #[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
#[IsGranted('role_permissions')]
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService, UserRepository $userRepository): Response public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService, UserRepository $userRepository): Response
{ {
$all = $this->roleRepository->findAll(); $all = $this->roleRepository->findAll();
@@ -167,7 +169,6 @@ final class PermissionController extends AbstractController
} }
#[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])] #[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])]
#[IsGranted('role_permissions')]
public function createRole(Request $request): Response public function createRole(Request $request): Response
{ {
$role = new Role(); $role = new Role();
@@ -200,8 +201,7 @@ final class PermissionController extends AbstractController
]); ]);
} }
#[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])] #[Route(path: '/roles/{role}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
#[IsGranted('role_permissions')]
public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) { if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
@@ -230,8 +230,7 @@ final class PermissionController extends AbstractController
return $this->redirectToRoute('admin_user_permissions'); return $this->redirectToRoute('admin_user_permissions');
} }
#[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])] #[Route(path: '/roles/{role}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
#[IsGranted('role_permissions')]
public function savePermission(Role $role, string $name, bool $value, string $csrfToken, PermissionService $permissionService, CsrfTokenManagerInterface $csrfTokenManager): Response public function savePermission(Role $role, string $name, bool $value, string $csrfToken, PermissionService $permissionService, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) { if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {

View File

@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
final class Date extends FunctionNode final class Date extends FunctionNode
{ {
private Node|string|null $value; private Node|string $value;
public function getSql(SqlWalker $sqlWalker): string public function getSql(SqlWalker $sqlWalker): string
{ {

View File

@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
final class Day extends FunctionNode final class Day extends FunctionNode
{ {
private Node|string|null $value; private Node|string $value;
public function getSql(SqlWalker $sqlWalker): string public function getSql(SqlWalker $sqlWalker): string
{ {

View File

@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
final class Month extends FunctionNode final class Month extends FunctionNode
{ {
private Node|string|null $value; private Node|string $value;
public function getSql(SqlWalker $sqlWalker): string public function getSql(SqlWalker $sqlWalker): string
{ {

View File

@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
final class Year extends FunctionNode final class Year extends FunctionNode
{ {
private Node|string|null $value; private Node|string $value;
public function getSql(SqlWalker $sqlWalker): string public function getSql(SqlWalker $sqlWalker): string
{ {

View File

@@ -10,8 +10,6 @@
namespace App\Entity; namespace App\Entity;
use App\Utils\Color; use App\Utils\Color;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
@@ -53,21 +51,8 @@ class Tag
use ColorTrait; use ColorTrait;
/**
* This is ONLY here, so we can count the amount of timesheets.
*
* See TagRepository "SIZE(t.timesheets)"
* Removing this makes the count more complicated.
* Should be refactored at some point in the future.
*
* @var Collection<Timesheet>
*/
#[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
private Collection $timesheets;
public function __construct() public function __construct()
{ {
$this->timesheets = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int

View File

@@ -200,7 +200,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
#[ORM\JoinTable(name: 'kimai2_timesheet_tags')] #[ORM\JoinTable(name: 'kimai2_timesheet_tags')]
#[ORM\JoinColumn(name: 'timesheet_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'timesheet_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'timesheets', cascade: ['persist'])] #[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])]
#[Assert\Valid] #[Assert\Valid]
private Collection $tags; private Collection $tags;
/** /**

View File

@@ -16,14 +16,17 @@ use App\Entity\User;
* - once per side load for page actions * - once per side load for page actions
* - once for every entity item (table row) * - once for every entity item (table row)
* *
* @property array{'actions': array, 'view': string} $payload * @property array{actions: array<string, string|array<mixed>>, view: string} $payload
*/ */
class PageActionsEvent extends ThemeEvent class PageActionsEvent extends ThemeEvent
{ {
private int $divider = 0; private int $divider = 0;
private ?string $locale = null; private ?string $locale = null;
public function __construct(User $user, array $payload, private string $action, private string $view) /**
* @param array<mixed> $payload
*/
public function __construct(User $user, array $payload, private readonly string $action, private readonly string $view)
{ {
// only for BC reasons, do not access it directly! // only for BC reasons, do not access it directly!
if (!\array_key_exists('actions', $payload)) { if (!\array_key_exists('actions', $payload)) {
@@ -58,8 +61,6 @@ class PageActionsEvent extends ThemeEvent
/** /**
* Custom view can only be table listings. * Custom view can only be table listings.
*
* @return bool
*/ */
public function isCustomView(): bool public function isCustomView(): bool
{ {
@@ -71,6 +72,9 @@ class PageActionsEvent extends ThemeEvent
return $this->view; return $this->view;
} }
/**
* @return array<string, array<mixed>>
*/
public function getActions(): array public function getActions(): array
{ {
$actions = $this->payload['actions']; $actions = $this->payload['actions'];
@@ -99,28 +103,38 @@ class PageActionsEvent extends ThemeEvent
public function hasSubmenu(string $submenu): bool public function hasSubmenu(string $submenu): bool
{ {
if (!$this->hasAction($submenu)) { if (!\array_key_exists($submenu, $this->payload['actions'])) {
return false; return false;
} }
return \array_key_exists('children', $this->payload['actions'][$submenu]); return \array_key_exists('children', $this->payload['actions'][$submenu]);
} }
/**
* @param array<string, mixed> $action
*/
public function addActionToSubmenu(string $submenu, string $key, array $action): void public function addActionToSubmenu(string $submenu, string $key, array $action): void
{ {
if ($this->hasAction($submenu)) { if (!\array_key_exists($submenu, $this->payload['actions'])) {
$this->payload['actions'][$submenu] = ['children' => []];
}
if (!\array_key_exists('children', $this->payload['actions'][$submenu])) { if (!\array_key_exists('children', $this->payload['actions'][$submenu])) {
$this->payload['actions'][$submenu]['children'] = []; $this->payload['actions'][$submenu]['children'] = [];
} }
}
$this->payload['actions'][$submenu]['children'][$key] = $action; $this->payload['actions'][$submenu]['children'][$key] = $action;
} }
/**
* @param array<string, mixed> $action
*/
public function replaceAction(string $key, array $action): void public function replaceAction(string $key, array $action): void
{ {
$this->payload['actions'][$key] = $action; $this->payload['actions'][$key] = $action;
} }
/**
* @param array<string, mixed> $action
*/
public function addAction(string $key, array $action): void public function addAction(string $key, array $action): void
{ {
if (!$this->hasAction($key)) { if (!$this->hasAction($key)) {

View File

@@ -28,7 +28,7 @@ class ThemeEvent extends Event
/** /**
* @param array<string, mixed|array<mixed>> $payload * @param array<string, mixed|array<mixed>> $payload
*/ */
public function __construct(private ?User $user = null, protected array $payload = []) public function __construct(private readonly ?User $user = null, protected array $payload = [])
{ {
} }

View File

@@ -14,42 +14,54 @@ use App\Validator\Constraints\Duration as DurationConstraint;
use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<string|int|null, string|null>
*/
final class DurationStringToSecondsTransformer implements DataTransformerInterface final class DurationStringToSecondsTransformer implements DataTransformerInterface
{ {
/** public function transform(mixed $value): ?string
* @param int $intToFormat
* @return string|null
*/
public function transform(mixed $intToFormat): ?string
{ {
if ($value === null) {
return null;
}
try { try {
return (new Duration())->format($intToFormat); if (!\is_int($value) && is_numeric($value)) {
$value = (int) $value;
}
if (!\is_int($value)) {
// do not throw an exception, that would break the frontend, make it null / empty instead
return null;
}
return (new Duration())->format($value);
} catch (\Exception | \TypeError $e) { } catch (\Exception | \TypeError $e) {
throw new TransformationFailedException($e->getMessage()); throw new TransformationFailedException($e->getMessage());
} }
} }
/** public function reverseTransform(mixed $value): ?int
* @param string|null $formatToInt
* @return int|null
*/
public function reverseTransform(mixed $formatToInt): ?int
{ {
if (null === $formatToInt) { if ($value === null) {
return null; return null;
} }
if (empty($formatToInt)) { if ($value === '') {
return 0; return 0;
} }
if (\is_int($value) || \is_float($value)) {
$value = (string) $value;
}
// we need this one here, because the data transformer is executed BEFORE the constraint is called // we need this one here, because the data transformer is executed BEFORE the constraint is called
if (!preg_match((new DurationConstraint())->pattern, $formatToInt)) { if (!preg_match((new DurationConstraint())->pattern, $value)) {
throw new TransformationFailedException('Invalid duration format given'); throw new TransformationFailedException('Invalid duration format given');
} }
try { try {
$seconds = (new Duration())->parseDurationString($formatToInt); $seconds = (new Duration())->parseDurationString($value);
// DateTime throws if a duration with too many seconds is passed and an amount of so // DateTime throws if a duration with too many seconds is passed and an amount of so
// many seconds is likely not required in a time-tracking application ;-) // many seconds is likely not required in a time-tracking application ;-)

View File

@@ -143,8 +143,8 @@ class ProjectStatisticService
) )
) )
) )
->setParameter('begin', $begin, Types::DATETIME_MUTABLE) ->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE)
->setParameter('end', $end, Types::DATETIME_MUTABLE) ->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE)
; ;
if (!$query->isIncludeNoWork()) { if (!$query->isIncludeNoWork()) {
@@ -323,14 +323,14 @@ class ProjectStatisticService
if ($begin !== null) { if ($begin !== null) {
$qb $qb
->andWhere($qb->expr()->gte('t.begin', ':begin')) ->andWhere($qb->expr()->gte('t.begin', ':begin'))
->setParameter('begin', $begin, Types::DATETIME_MUTABLE) ->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE)
; ;
} }
if ($end !== null) { if ($end !== null) {
$qb $qb
->andWhere($qb->expr()->lte('t.begin', ':end')) ->andWhere($qb->expr()->lte('t.begin', ':end'))
->setParameter('end', $end, Types::DATETIME_MUTABLE) ->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE)
; ;
} }
@@ -646,7 +646,7 @@ class ProjectStatisticService
) )
) )
->addGroupBy('p') ->addGroupBy('p')
->setParameter('project_end', $today, Types::DATETIME_MUTABLE) ->setParameter('project_end', DateTimeImmutable::createFromInterface($today), Types::DATETIME_IMMUTABLE)
; ;
if ($query->getCustomer() !== null) { if ($query->getCustomer() !== null) {
@@ -732,7 +732,7 @@ class ProjectStatisticService
$qb = clone $tplQb; $qb = clone $tplQb;
$qb $qb
->andWhere('DATE(t.date) = :start_date') ->andWhere('DATE(t.date) = :start_date')
->setParameter('start_date', $today, Types::DATETIME_MUTABLE) ->setParameter('start_date', DateTimeImmutable::createFromInterface($today), Types::DATETIME_IMMUTABLE)
; ;
$result = $qb->getQuery()->getScalarResult(); $result = $qb->getQuery()->getScalarResult();
@@ -744,8 +744,8 @@ class ProjectStatisticService
$qb = clone $tplQb; $qb = clone $tplQb;
$qb $qb
->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date') ->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date')
->setParameter('start_date', $startOfWeek, Types::DATETIME_MUTABLE) ->setParameter('start_date', DateTimeImmutable::createFromInterface($startOfWeek), Types::DATETIME_IMMUTABLE)
->setParameter('end_date', $endOfWeek, Types::DATETIME_MUTABLE) ->setParameter('end_date', DateTimeImmutable::createFromInterface($endOfWeek), Types::DATETIME_IMMUTABLE)
; ;
$result = $qb->getQuery()->getScalarResult(); $result = $qb->getQuery()->getScalarResult();
@@ -757,8 +757,8 @@ class ProjectStatisticService
$qb = clone $tplQb; $qb = clone $tplQb;
$qb $qb
->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date') ->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date')
->setParameter('start_date', $startMonth, Types::DATETIME_MUTABLE) ->setParameter('start_date', DateTimeImmutable::createFromInterface($startMonth), Types::DATETIME_IMMUTABLE)
->setParameter('end_date', $endMonth, Types::DATETIME_MUTABLE) ->setParameter('end_date', DateTimeImmutable::createFromInterface($endMonth), Types::DATETIME_IMMUTABLE)
; ;
$result = $qb->getQuery()->getScalarResult(); $result = $qb->getQuery()->getScalarResult();

View File

@@ -91,6 +91,9 @@ class ActivityRepository extends EntityRepository
return $this->count([]); return $this->count([]);
} }
/**
* @param array<Team> $teams
*/
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): void private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): void
{ {
$permissions = $this->getPermissionCriteria($qb, $user, $teams, $globalsOnly); $permissions = $this->getPermissionCriteria($qb, $user, $teams, $globalsOnly);
@@ -99,6 +102,9 @@ class ActivityRepository extends EntityRepository
} }
} }
/**
* @param array<Team> $teams
*/
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): Andx private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): Andx
{ {
$andX = $qb->expr()->andX(); $andX = $qb->expr()->andX();

View File

@@ -14,36 +14,46 @@ use App\Entity\User;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
/** /**
* @extends \Doctrine\ORM\EntityRepository<Bookmark> * @extends EntityRepository<Bookmark>
*/ */
class BookmarkRepository extends EntityRepository class BookmarkRepository extends EntityRepository
{ {
/** @var array<string, array<string, array<string, Bookmark>>> */
private array $userCache = []; private array $userCache = [];
public function saveBookmark(Bookmark $bookmark) public function saveBookmark(Bookmark $bookmark): void
{ {
$entityManager = $this->getEntityManager(); $entityManager = $this->getEntityManager();
$entityManager->persist($bookmark); $entityManager->persist($bookmark);
$entityManager->flush(); $entityManager->flush();
if ($bookmark->getUser()) {
$this->clearCache($bookmark->getUser()); $this->clearCache($bookmark->getUser());
} }
}
private function clearCache(User $user): void private function clearCache(?User $user): void
{ {
if ($user === null || $user->getId() === null) {
return;
}
$key = 'user_' . $user->getId(); $key = 'user_' . $user->getId();
if (\array_key_exists($key, $this->userCache)) { if (\array_key_exists($key, $this->userCache)) {
unset($this->userCache[$key]); unset($this->userCache[$key]);
} }
} }
public function deleteBookmark(Bookmark $bookmark) public function deleteBookmark(Bookmark $bookmark): void
{ {
$em = $this->getEntityManager(); $em = $this->getEntityManager();
$em->remove($bookmark); $em->remove($bookmark);
$em->flush(); $em->flush();
if ($bookmark->getUser()) {
$this->clearCache($bookmark->getUser()); $this->clearCache($bookmark->getUser());
} }
}
public function getSearchDefaultOptions(User $user, string $name): ?Bookmark public function getSearchDefaultOptions(User $user, string $name): ?Bookmark
{ {
@@ -59,7 +69,7 @@ class BookmarkRepository extends EntityRepository
$this->userCache[$key] = []; $this->userCache[$key] = [];
$all = $this->findBy(['user' => $user->getId()]); $all = $this->findBy(['user' => $user->getId()]);
foreach ($all as $item) { foreach ($all as $item) {
$this->userCache[$key][$item->getType()][mb_substr($item->getName(), 0, 50)] = $item; $this->userCache[$key][$item->getType()][mb_substr($item->getName() ?? '__DEFAULT__', 0, 50)] = $item;
} }
} }

View File

@@ -79,6 +79,9 @@ class CustomerRepository extends EntityRepository
return $this->count([]); return $this->count([]);
} }
/**
* @param array<Team> $teams
*/
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
{ {
$permissions = $this->getPermissionCriteria($qb, $user, $teams); $permissions = $this->getPermissionCriteria($qb, $user, $teams);
@@ -87,6 +90,9 @@ class CustomerRepository extends EntityRepository
} }
} }
/**
* @param array<Team> $teams
*/
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
{ {
$andX = $qb->expr()->andX(); $andX = $qb->expr()->andX();
@@ -349,6 +355,9 @@ class CustomerRepository extends EntityRepository
} }
} }
/**
* @return array<CustomerComment>
*/
public function getComments(Customer $customer): array public function getComments(Customer $customer): array
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -83,13 +83,14 @@ class InvoiceRepository extends EntityRepository
; ;
} }
/** @var array{'counter': int|numeric-string}|null $result */
$result = $qb->getQuery()->getOneOrNullResult(); $result = $qb->getQuery()->getOneOrNullResult();
if ($result === null) { if ($result === null) {
return 0; return 0;
} }
return $result['counter']; return (int) $result['counter'];
} }
public function getCounterForDay(\DateTimeInterface $date, ?Customer $customer = null, ?User $user = null): int public function getCounterForDay(\DateTimeInterface $date, ?Customer $customer = null, ?User $user = null): int
@@ -137,7 +138,10 @@ class InvoiceRepository extends EntityRepository
return $this->count([]); return $this->count([]);
} }
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []) /**
* @param array<Team> $teams
*/
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
{ {
// make sure that all queries without a user see all projects // make sure that all queries without a user see all projects
if (null === $user && empty($teams)) { if (null === $user && empty($teams)) {

View File

@@ -71,7 +71,7 @@ final class TimesheetLoader implements LoaderInterface
if ($this->fullyHydrated) { if ($this->fullyHydrated) {
$customerIds = array_filter(array_unique(array_map(function (Project $project) { $customerIds = array_filter(array_unique(array_map(function (Project $project) {
return $project->getCustomer()->getId(); return $project->getCustomer()?->getId();
}, $projects)), function ($value) { return $value !== null; }); }, $projects)), function ($value) { return $value !== null; });
$qb = $em->createQueryBuilder(); $qb = $em->createQueryBuilder();

View File

@@ -13,6 +13,7 @@ use Pagerfanta\Adapter\AdapterInterface;
/** /**
* @template-covariant T * @template-covariant T
* @extends AdapterInterface<T>
*/ */
interface PaginatorInterface extends AdapterInterface interface PaginatorInterface extends AdapterInterface
{ {

View File

@@ -85,6 +85,9 @@ class ProjectRepository extends EntityRepository
return $this->count([]); return $this->count([]);
} }
/**
* @param array<Team> $teams
*/
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
{ {
$permissions = $this->getPermissionCriteria($qb, $user, $teams); $permissions = $this->getPermissionCriteria($qb, $user, $teams);
@@ -93,6 +96,9 @@ class ProjectRepository extends EntityRepository
} }
} }
/**
* @param array<Team> $teams
*/
private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx private function getPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): Andx
{ {
$andX = $qb->expr()->andX(); $andX = $qb->expr()->andX();
@@ -429,6 +435,9 @@ class ProjectRepository extends EntityRepository
} }
} }
/**
* @return array<ProjectComment>
*/
public function getComments(Project $project): array public function getComments(Project $project): array
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -40,12 +40,12 @@ trait RepositorySearchTrait
private function addSearchTerm(QueryBuilder $qb, BaseQuery $query): void private function addSearchTerm(QueryBuilder $qb, BaseQuery $query): void
{ {
if (!$query->hasSearchTerm()) { $searchTerm = $query->getSearchTerm();
if ($searchTerm === null) {
return; return;
} }
$searchTerm = $query->getSearchTerm();
if (!$this->supportsMetaFields() && !$searchTerm->hasSearchTerm()) { if (!$this->supportsMetaFields() && !$searchTerm->hasSearchTerm()) {
return; return;
} }

View File

@@ -67,6 +67,9 @@ final class TimesheetResult
return $this->statisticCache; return $this->statisticCache;
} }
/**
* @return iterable<Timesheet>
*/
public function toIterable(): iterable public function toIterable(): iterable
{ {
return $this->query->toIterable(); return $this->query->toIterable();

View File

@@ -10,6 +10,7 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Tag; use App\Entity\Tag;
use App\Entity\Timesheet;
use App\Repository\Paginator\QueryPaginator; use App\Repository\Paginator\QueryPaginator;
use App\Repository\Query\TagFormTypeQuery; use App\Repository\Query\TagFormTypeQuery;
use App\Repository\Query\TagQuery; use App\Repository\Query\TagQuery;
@@ -131,7 +132,11 @@ class TagRepository extends EntityRepository
{ {
$qb = $this->createQueryBuilder('tag'); $qb = $this->createQueryBuilder('tag');
$qb->select('tag.id, tag.name, tag.color, tag.visible, SIZE(tag.timesheets) as amount'); $qb1 = $this->getEntityManager()->createQueryBuilder();
$qb1->from(Timesheet::class, 't')->select('COUNT(tags)')->innerJoin('t.tags', 'tags')->where('tags.id = tag.id');
$qb->select('tag.id, tag.name, tag.color, tag.visible');
$qb->addSelect('(' . $qb1->getDQL() . ') as amount');
$orderBy = $query->getOrderBy(); $orderBy = $query->getOrderBy();
$orderBy = match ($orderBy) { $orderBy = match ($orderBy) {
@@ -149,8 +154,8 @@ class TagRepository extends EntityRepository
$qb->addOrderBy($orderBy, $query->getOrder()); $qb->addOrderBy($orderBy, $query->getOrder());
if ($query->hasSearchTerm()) {
$searchTerm = $query->getSearchTerm(); $searchTerm = $query->getSearchTerm();
if ($searchTerm !== null) {
$searchAnd = $qb->expr()->andX(); $searchAnd = $qb->expr()->andX();
if ($searchTerm->hasSearchTerm()) { if ($searchTerm->hasSearchTerm()) {

View File

@@ -117,14 +117,14 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
*/ */
public function findOneBy(array $criteria, array $orderBy = null): ?object public function findOneBy(array $criteria, array $orderBy = null): ?object
{ {
if (\count($criteria) === 1 && isset($criteria['username'])) { if (\count($criteria) === 1 && isset($criteria['username']) && \is_string($criteria['username'])) {
return $this->loadUserByIdentifier($criteria['username']); return $this->loadUserByIdentifier($criteria['username']);
} }
return parent::findOneBy($criteria, $orderBy); return parent::findOneBy($criteria, $orderBy);
} }
public function findByUsername($username): ?User public function findByUsername(string $username): ?User
{ {
return parent::findOneBy(['username' => $username]); return parent::findOneBy(['username' => $username]);
} }
@@ -321,9 +321,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
$qb->setParameter('system', $query->getSystemAccount(), Types::BOOLEAN); $qb->setParameter('system', $query->getSystemAccount(), Types::BOOLEAN);
} }
if ($query->hasSearchTerm()) {
$searchAnd = $qb->expr()->andX();
$searchTerm = $query->getSearchTerm(); $searchTerm = $query->getSearchTerm();
if ($searchTerm !== null) {
$searchAnd = $qb->expr()->andX();
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) { foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
$qb->leftJoin('u.preferences', 'meta'); $qb->leftJoin('u.preferences', 'meta');

View File

@@ -17,13 +17,13 @@ use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class RoundingService final class RoundingService
{ {
/** /**
* @var array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}> * @var null|non-empty-array<array-key, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}>
*/ */
private ?array $rulesCache = null; private ?array $rulesCache = null;
/** /**
* @param RoundingInterface[] $roundingModes * @param RoundingInterface[] $roundingModes
* @param array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}> $rules * @param array<string, array{'days': string, 'begin': int, 'end': int, 'duration': int, 'mode': string}> $rules
*/ */
public function __construct( public function __construct(
private readonly SystemConfiguration $configuration, private readonly SystemConfiguration $configuration,
@@ -35,13 +35,13 @@ final class RoundingService
} }
/** /**
* @return array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}> * @return non-empty-array<array-key, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}>
*/ */
private function getRoundingRules(): array private function getRoundingRules(): array
{ {
if ($this->rulesCache === null) { if ($this->rulesCache === null) {
$rules = $this->rules; $rules = $this->rules;
$rules['default']['days'] = $this->configuration->getTimesheetDefaultRoundingDays(); $rules['default']['days'] = $this->parseDays($this->configuration->getTimesheetDefaultRoundingDays());
$rules['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin(); $rules['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin();
$rules['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd(); $rules['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd();
$rules['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration(); $rules['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration();
@@ -49,17 +49,13 @@ final class RoundingService
// see AppExtension, conversion from string to array due to system configuration not allowing to store arrays // see AppExtension, conversion from string to array due to system configuration not allowing to store arrays
foreach ($rules as $key => $settings) { foreach ($rules as $key => $settings) {
if (\is_array($settings['days'])) { if (\is_string($settings['days'])) {
continue;
}
if ($settings['days'] === '') { if ($settings['days'] === '') {
$rules[$key]['days'] = []; $rules[$key]['days'] = [];
continue; continue;
} }
$days = explode(',', $settings['days']); $rules[$key]['days'] = array_map('strtolower', array_map('trim', explode(',', $settings['days'])));
$days = array_map('trim', $days); }
$days = array_map('strtolower', $days);
$rules[$key]['days'] = $days;
} }
$this->rulesCache = $rules; // @phpstan-ignore-line $this->rulesCache = $rules; // @phpstan-ignore-line
} }
@@ -67,6 +63,14 @@ final class RoundingService
return $this->rulesCache; // @phpstan-ignore-line return $this->rulesCache; // @phpstan-ignore-line
} }
/**
* @return array<string>
*/
private function parseDays(string $days): array
{
return array_map('strtolower', array_map('trim', explode(',', $days)));
}
public function roundBegin(Timesheet $record): void public function roundBegin(Timesheet $record): void
{ {
foreach ($this->getRoundingRules() as $rounding) { foreach ($this->getRoundingRules() as $rounding) {

View File

@@ -93,7 +93,7 @@
{% endif %} {% endif %}
{% if year is defined %} {% if year is defined %}
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'working_time_detail_box', border: false, item: {options: {bodyExtraClass: 'border-top', open: not box_configuration.collapsed}, bodyClasses: 'p-0 table-responsive'}} %} {% embed '@theme/embeds/collapsible.html.twig' with {id: 'working_time_detail_box', border: false, item: {options: {bodyExtraClass: 'border-top', open: true}, bodyClasses: 'p-0 table-responsive'}} %}
{% set yearShould = 0 %} {% set yearShould = 0 %}
{% set yearIs = 0 %} {% set yearIs = 0 %}
{% set approved = 0 %} {% set approved = 0 %}

View File

@@ -34,7 +34,7 @@
{{ role.getName()|trans }} {{ role.getName()|trans }}
{% if canEditPermissions and (role.name not in system_roles) %} {% if canEditPermissions and (role.name not in system_roles) %}
&nbsp; &nbsp;
<a href="{{ path('admin_user_role_delete', {'id': role.id, 'csrfToken': token}) }}" class="confirmation-link" data-question="confirm.delete">{{ icon('trash') }}</a> <a href="{{ path('admin_user_role_delete', {'role': role.id, 'csrfToken': token}) }}" class="confirmation-link" data-question="confirm.delete">{{ icon('trash') }}</a>
{% endif %} {% endif %}
</h4> </h4>
<div class="list-unstyled d-flex mb-0"> <div class="list-unstyled d-flex mb-0">
@@ -82,10 +82,10 @@
{% if value %} {% if value %}
{{ widgets.label('yes'|trans, 'warning') }} {{ widgets.label('yes'|trans, 'warning') }}
{% else %} {% else %}
<a class="togglePerm permOff" href="#" data-href="{{ path('admin_user_permission_save', {'id': role.id, 'name': permission, 'value': '1', 'csrfToken': '__TOKEN__'}) }}">{{ widgets.label('no'|trans, 'danger') }}</a> <a class="togglePerm permOff" href="#" data-href="{{ path('admin_user_permission_save', {'role': role.id, 'name': permission, 'value': '1', 'csrfToken': '__TOKEN__'}) }}">{{ widgets.label('no'|trans, 'danger') }}</a>
{% endif %} {% endif %}
{% else %} {% else %}
<a class="togglePerm {{ value ? 'permOn' : 'permOff' }}" href="#" data-href="{{ path('admin_user_permission_save', {'id': role.id, 'name': permission, 'value': '__VALUE__', 'csrfToken': '__TOKEN__'}) }}">{{ widgets.label_boolean(value) }}</a> <a class="togglePerm {{ value ? 'permOn' : 'permOff' }}" href="#" data-href="{{ path('admin_user_permission_save', {'role': role.id, 'name': permission, 'value': '__VALUE__', 'csrfToken': '__TOKEN__'}) }}">{{ widgets.label_boolean(value) }}</a>
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}

View File

@@ -2,7 +2,7 @@
{% block report_form_layout %} {% block report_form_layout %}
{{ form_widget(form.date) }} {{ form_widget(form.date) }}
{{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.customer, {'label': false, 'placeholder': 'customer'}) }}
{{ form_widget(form.sumType) }} {{ form_widget(form.sumType) }}
{% from '@theme/components/buttons.html.twig' import submit_button %} {% from '@theme/components/buttons.html.twig' import submit_button %}
{{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }} {{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }}

View File

@@ -46,7 +46,7 @@
{% block report_form_layout %} {% block report_form_layout %}
{{ form_widget(form.month, {'label': false}) }} {{ form_widget(form.month, {'label': false}) }}
{{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.customer, {'label': false, 'placeholder': 'customer'}) }}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('filter', true) }} {{ icon('filter', true) }}

View File

@@ -55,7 +55,7 @@
{% endblock %} {% endblock %}
{% block report_form_layout %} {% block report_form_layout %}
{{ form_widget(form.project, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.project, {'label': false, 'placeholder': 'project'}) }}
{% endblock %} {% endblock %}
{% block report %} {% block report %}

View File

@@ -1,7 +1,7 @@
{% extends 'reporting/project_list_data.html.twig' %} {% extends 'reporting/project_list_data.html.twig' %}
{% block report_form_layout %} {% block report_form_layout %}
{{ form_widget(form.customer, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.customer, {'label': false, 'placeholder': 'customer'}) }}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('filter', true) }} {{ icon('filter', true) }}

View File

@@ -3,10 +3,10 @@
{% block report_form_layout %} {% block report_form_layout %}
{{ form_widget(form.date) }} {{ form_widget(form.date) }}
{% if form.team is defined %} {% if form.team is defined %}
{{ form_widget(form.team, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.team, {'label': false, 'placeholder': 'team'}) }}
{% endif %} {% endif %}
{% if form.project is defined %} {% if form.project is defined %}
{{ form_widget(form.project, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.project, {'label': false, 'placeholder': 'project'}) }}
{% endif %} {% endif %}
{{ form_widget(form.sumType) }} {{ form_widget(form.sumType) }}
{% from '@theme/components/buttons.html.twig' import submit_button %} {% from '@theme/components/buttons.html.twig' import submit_button %}

View File

@@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace API\Authentication; namespace App\Tests\API\Authentication;
use App\API\Authentication\AccessTokenHandler; use App\API\Authentication\AccessTokenHandler;
use App\Entity\AccessToken; use App\Entity\AccessToken;

View File

@@ -7,11 +7,10 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace API; namespace App\Tests\API;
use App\DataFixtures\UserFixtures; use App\DataFixtures\UserFixtures;
use App\Entity\User; use App\Entity\User;
use App\Tests\API\APIControllerBaseTest;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
/** /**

View File

@@ -178,18 +178,19 @@ abstract class ControllerBaseTest extends WebTestCase
/** @var RedirectResponse $response */ /** @var RedirectResponse $response */
$response = $client->getResponse(); $response = $client->getResponse();
self::assertInstanceOf(RedirectResponse::class, $response);
self::assertTrue( self::assertTrue(
$response->isRedirect(), $response->isRedirect(),
\sprintf('The secure URL %s is not protected.', $url) \sprintf('The URL %s is not protected (%s occurred).', $url, $response->getStatusCode())
); );
self::assertStringEndsWith( self::assertStringEndsWith(
'/login', '/login',
$response->getTargetUrl(), $response->getTargetUrl(),
\sprintf('The secure URL %s does not redirect to the login form.', $url) \sprintf('The URL %s does not redirect to the login form.', $url)
); );
self::assertInstanceOf(RedirectResponse::class, $response);
} }
protected function assertSuccessResponse(HttpKernelBrowser $client, string $message = ''): void protected function assertSuccessResponse(HttpKernelBrowser $client, string $message = ''): void

View File

@@ -18,27 +18,26 @@ use Symfony\Component\Form\Exception\TransformationFailedException;
*/ */
class DurationStringToSecondsTransformerTest extends TestCase class DurationStringToSecondsTransformerTest extends TestCase
{ {
/** private DurationStringToSecondsTransformer $sut;
* @var DurationStringToSecondsTransformer
*/
private $sut;
protected function setUp(): void protected function setUp(): void
{ {
$this->sut = new DurationStringToSecondsTransformer(); $this->sut = new DurationStringToSecondsTransformer();
} }
public function getValidTestDataTransform() public function getValidTestDataTransform(): array
{ {
return [ return [
['0:00', '0'], ['0:00', '0'],
['123:00', '442800'],
['123:00', 442800],
['0:00', 0], ['0:00', 0],
['2:00', 7213], // by default no seconds are returned ['2:00', 7213], // by default no seconds are returned
[null, null], [null, null],
]; ];
} }
public function getInvalidTestDataTransform() public function getInvalidTestDataTransform(): array
{ {
return [ return [
[''], [''],
@@ -59,12 +58,14 @@ class DurationStringToSecondsTransformerTest extends TestCase
*/ */
public function testInvalidTransformThrowsException($transform): void public function testInvalidTransformThrowsException($transform): void
{ {
$this->expectException(TransformationFailedException::class); $value = $this->sut->transform($transform);
$this->assertNull($value);
$this->sut->transform($transform);
} }
public function getValidTestDataReverseTransform() /**
* @return array<int, array<int, string|int|float|null>>
*/
public function getValidTestDataReverseTransform(): array
{ {
return [ return [
['2h3s', 7203], ['2h3s', 7203],
@@ -72,16 +73,23 @@ class DurationStringToSecondsTransformerTest extends TestCase
['0', null], ['0', null],
[null, null], [null, null],
['87600000000:00:00', 315360000000000], ['87600000000:00:00', 315360000000000],
[87600000000, 315360000000000], // int will be treated we could argue if this is a correct behavior
[87599999999.5, 315359999998200], // only int can be used as hourly input
]; ];
} }
public function getInvalidTestDataReverseTransform() /**
* @return array<int, array<int, string|int>>
*/
public function getInvalidTestDataReverseTransform(): array
{ {
return [ return [
['xxx'], ['xxx'],
[':::'], [':::'],
['0::0'], ['0::0'],
// too large
['87600000000:00:01'], ['87600000000:00:01'],
// too large
[315360000000001], [315360000000001],
]; ];
} }

View File

@@ -58,7 +58,7 @@ class TranslationsTest extends TestCase
foreach ($body->children() as $transUnit) { foreach ($body->children() as $transUnit) {
preg_match_all('/%[a-zA-Z]{1,}%/Uu', (string) $transUnit->target, $matches); preg_match_all('/%[a-zA-Z]{1,}%/Uu', (string) $transUnit->target, $matches);
if (!empty($matches) && !empty($matches[0])) { if (!empty($matches[0])) {
asort($matches[0]); asort($matches[0]);
$trans[(string) $transUnit->source] = array_values($matches[0]); $trans[(string) $transUnit->source] = array_values($matches[0]);
} }

View File

@@ -18,6 +18,7 @@ parameters:
doctrine: doctrine:
objectManagerLoader: %rootDir%/../../../tests/phpstan-doctrine.php objectManagerLoader: %rootDir%/../../../tests/phpstan-doctrine.php
ignoreErrors: ignoreErrors:
- identifier: missingType.iterableValue
- -
message: "#^Method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:assertApiException\\(\\) has parameter \\$expectedErrors with no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:assertApiException\\(\\) has parameter \\$expectedErrors with no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -2108,26 +2109,6 @@ parameters:
count: 1 count: 1
path: Form/CustomerEditFormTest.php path: Form/CustomerEditFormTest.php
-
message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:getInvalidTestDataReverseTransform\\(\\) has no return type specified\\.$#"
count: 1
path: Form/DataTransformer/DurationStringToSecondsTransformerTest.php
-
message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:getInvalidTestDataTransform\\(\\) has no return type specified\\.$#"
count: 1
path: Form/DataTransformer/DurationStringToSecondsTransformerTest.php
-
message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:getValidTestDataReverseTransform\\(\\) has no return type specified\\.$#"
count: 1
path: Form/DataTransformer/DurationStringToSecondsTransformerTest.php
-
message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:getValidTestDataTransform\\(\\) has no return type specified\\.$#"
count: 1
path: Form/DataTransformer/DurationStringToSecondsTransformerTest.php
- -
message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:testInvalidReverseTransformThrowsException\\(\\) has parameter \\$transform with no type specified\\.$#" message: "#^Method App\\\\Tests\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformerTest\\:\\:testInvalidReverseTransformThrowsException\\(\\) has parameter \\$transform with no type specified\\.$#"
count: 1 count: 1

View File

@@ -278,6 +278,10 @@
<source>created_at</source> <source>created_at</source>
<target>Erstellt am</target> <target>Erstellt am</target>
</trans-unit> </trans-unit>
<trans-unit id="QG7.sen" resname="created">
<source>Created</source>
<target>Erstellt</target>
</trans-unit>
<trans-unit id="DodjLNR" resname="date"> <trans-unit id="DodjLNR" resname="date">
<source>date</source> <source>date</source>
<target>Datum</target> <target>Datum</target>
@@ -1254,10 +1258,18 @@
<source>status.approved</source> <source>status.approved</source>
<target>Genehmigt</target> <target>Genehmigt</target>
</trans-unit> </trans-unit>
<trans-unit id="dOIWgOr" resname="approve">
<source>approve</source>
<target>Genehmigen</target>
</trans-unit>
<trans-unit id="Na24cyT" resname="status.requested"> <trans-unit id="Na24cyT" resname="status.requested">
<source>status.requested</source> <source>status.requested</source>
<target>Beantragt</target> <target>Beantragt</target>
</trans-unit> </trans-unit>
<trans-unit id="H1i5FFs" resname="request">
<source>request</source>
<target>Beantragen</target>
</trans-unit>
<trans-unit id="crrlEUA" resname="preview.skipped_rows"> <trans-unit id="crrlEUA" resname="preview.skipped_rows">
<source>preview.skipped_rows</source> <source>preview.skipped_rows</source>
<target>Überspringe Vorschau von %rows% weiteren Zeilen …</target> <target>Überspringe Vorschau von %rows% weiteren Zeilen …</target>
@@ -1470,6 +1482,10 @@
<source>Updated at</source> <source>Updated at</source>
<target>Aktualisiert am</target> <target>Aktualisiert am</target>
</trans-unit> </trans-unit>
<trans-unit id="J_teUVB" resname="updated">
<source>Updated</source>
<target>Aktualisiert</target>
</trans-unit>
<trans-unit id="nlwMf5w" resname="invoice_document.max_reached"> <trans-unit id="nlwMf5w" resname="invoice_document.max_reached">
<source>invoice_document.max_reached</source> <source>invoice_document.max_reached</source>
<target>Maximale Anzahl von %max% Rechnungsdokumenten erreicht. Um weitere hinzufügen müssen Sie zunächst eins löschen.</target> <target>Maximale Anzahl von %max% Rechnungsdokumenten erreicht. Um weitere hinzufügen müssen Sie zunächst eins löschen.</target>

View File

@@ -278,6 +278,10 @@
<source>created_at</source> <source>created_at</source>
<target>Created at</target> <target>Created at</target>
</trans-unit> </trans-unit>
<trans-unit id="QG7.sen" resname="created">
<source>Created</source>
<target>Created</target>
</trans-unit>
<trans-unit id="DodjLNR" resname="date"> <trans-unit id="DodjLNR" resname="date">
<source>date</source> <source>date</source>
<target>Date</target> <target>Date</target>
@@ -1254,10 +1258,18 @@
<source>status.approved</source> <source>status.approved</source>
<target>Approved</target> <target>Approved</target>
</trans-unit> </trans-unit>
<trans-unit id="dOIWgOr" resname="approve">
<source>approve</source>
<target>Approve</target>
</trans-unit>
<trans-unit id="Na24cyT" resname="status.requested"> <trans-unit id="Na24cyT" resname="status.requested">
<source>status.requested</source> <source>status.requested</source>
<target>Requested</target> <target>Requested</target>
</trans-unit> </trans-unit>
<trans-unit id="H1i5FFs" resname="request">
<source>request</source>
<target>Request</target>
</trans-unit>
<trans-unit id="crrlEUA" resname="preview.skipped_rows"> <trans-unit id="crrlEUA" resname="preview.skipped_rows">
<source>Skipped preview of %rows% more rows …</source> <source>Skipped preview of %rows% more rows …</source>
<target state="translated">Skipped preview of %rows% more rows …</target> <target state="translated">Skipped preview of %rows% more rows …</target>
@@ -1470,6 +1482,10 @@
<source>Updated at</source> <source>Updated at</source>
<target>Updated at</target> <target>Updated at</target>
</trans-unit> </trans-unit>
<trans-unit id="J_teUVB" resname="updated">
<source>Updated</source>
<target>Updated</target>
</trans-unit>
<trans-unit id="nlwMf5w" resname="invoice_document.max_reached"> <trans-unit id="nlwMf5w" resname="invoice_document.max_reached">
<source>invoice_document.max_reached</source> <source>invoice_document.max_reached</source>
<target>Reached maximum amount of %max% invoice documents. You can add more after removing one.</target> <target>Reached maximum amount of %max% invoice documents. You can add more after removing one.</target>

View File

@@ -1110,6 +1110,10 @@
<source>Updated at</source> <source>Updated at</source>
<target state="translated">Zaktualizowano</target> <target state="translated">Zaktualizowano</target>
</trans-unit> </trans-unit>
<trans-unit id="J_teUVB" resname="updated">
<source>Updated</source>
<target>Zaktualizowany</target>
</trans-unit>
<trans-unit id="nlwMf5w" resname="invoice_document.max_reached"> <trans-unit id="nlwMf5w" resname="invoice_document.max_reached">
<source>invoice_document.max_reached</source> <source>invoice_document.max_reached</source>
<target state="translated">Osiągnięto maksymalną ilość z %max% faktur. Możesz dodać więcej po usunięciu jednej.</target> <target state="translated">Osiągnięto maksymalną ilość z %max% faktur. Możesz dodać więcej po usunięciu jednej.</target>
@@ -1434,6 +1438,10 @@
<source>created_at</source> <source>created_at</source>
<target state="translated">Utworzono o</target> <target state="translated">Utworzono o</target>
</trans-unit> </trans-unit>
<trans-unit id="QG7.sen" resname="created">
<source>Created</source>
<target>Stworzony</target>
</trans-unit>
<trans-unit id="eCEGRzN" resname="work_contract"> <trans-unit id="eCEGRzN" resname="work_contract">
<source>work_contract</source> <source>work_contract</source>
<target state="translated">Umowa o pracę</target> <target state="translated">Umowa o pracę</target>