Release 2.21.0 (#5014)
This commit is contained in:
@@ -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'));
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
34
composer.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
|
||||||
|
|||||||
251
phpstan.neon
251
phpstan.neon
@@ -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
|
||||||
|
|||||||
2
public/build/app.13d5a0d9.js
Normal file
2
public/build/app.13d5a0d9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'])]
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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 = [])
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ;-)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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) %}
|
||||||
|
|
||||||
<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 %}
|
||||||
|
|||||||
@@ -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') }}
|
||||||
|
|||||||
@@ -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) }}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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) }}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user