0
0
Fork 0
mirror of https://github.com/kevinpapst/kimai2.git synced 2025-04-11 00:00:26 +00:00

release 2.0 beta ()

* remove twitter link
* remove WIP file
* adjust release draft message
* reset code coverage threshold back to 0.5
* changed wordings
* re-activate wizard for fixture accounts
* fix repo url and license
* license identifier
* bump version
* moved Kimai 1 import command from core to plugin
* do not traverse into invoice template subdirectories ()
* fix branch alias
* composer update
* switch language on wizard select
* new twig function to create qr code
* fix daily stats in timesheet listing
* improved html invoice templates
This commit is contained in:
Kevin Papst 2023-01-12 12:10:11 +01:00 committed by GitHub
parent cbd65f1f1d
commit 8069e332fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 386 additions and 2850 deletions

View file

@ -9,7 +9,7 @@ coverage:
status:
project:
default:
threshold: 2.5%
threshold: 0.5%
patch: off
changes: no

View file

@ -35,11 +35,7 @@ version-resolver:
template: |
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
**PHP Version compatibility:**
- PHP 7.3 and PHP 7.4 are [end-of-life](https://www.php.net/supported-versions.php)
- PHP 8.0 and PHP 8.1 are supported
A feature freeze is in place and only bugfix releases will be published for 1.30.x. Next major release will be in the 2.x series (PHP >= 8.1, Symfony 6, Tabler UI, see #2902).
**Compatible with PHP 8.1 and 8.2**
$CHANGES

View file

@ -16,10 +16,10 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
- uses: dessant/lock-threads@v4
with:
github-token: ${{ github.token }}
issue-inactive-days: '180'
issue-inactive-days: '90'
exclude-issue-created-before: ''
exclude-issue-created-after: ''
exclude-issue-created-between: ''

View file

@ -5,9 +5,8 @@
<p align="center">
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
<a href="https://twitter.com/kimai_org" rel="me"><img alt="Twitter" src="https://img.shields.io/badge/follow-%40kimai__org-00acee"></a>
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
</p>
@ -75,11 +74,11 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
In case you want to contribute, but you wouldn't know how, here are some suggestions:
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps!
- Answer questions: You know the answer to another user's problem? Share your knowledge!
- Make a feature request: Something can be done better? Something essential missing? Let us know!
- Report bugs
- You don't have to be programmer to help. The documentation and translation could use some love as well.
- Sponsor the project, free software still costs money
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can toot or tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps!
- Answer questions: You know the answer to another user's problem? Share your knowledge.
- Something can be done better? An essential feature is missing? Create a feature request.
- Report bugs: that shouldn't happen too often.
- You don't have to be programmer, the documentation and translation could use some love as well.
- Sponsor the project: free software costs money to create!
There is one simple rule in our "Code of conduct": Don't be an ass!

36
TODO
View file

@ -1,36 +0,0 @@
===========================================================================
INVOICES
- HTML Rechnungstemplates funktionieren nicht mehr richtig
===========================================================================
THEME
- Update to latest release und angepasstes CSS entfernen
- Theme Dark Mode mit Modus "Browser" sollte Standard sein:
https://github.com/tabler/tabler/issues/892#event-5666309557
===========================================================================
MIXED
- Alle URLs ändern
- /admin/activity/ zu /activity/
- /admin/project/ zu /project/
- /admin/..../ zu /..../
- /team/timesheet/ zu /timesheets/
- Tags => immer Berechtigung prüfen und "create" option über die VIEW ans Javascript geben
=> den FormType nur ändern zu "AutoComplete" wenn es mehr als 500 Tags sind
=> Übersetzungen (Suche, Erstellen, Keine Ergebnisse) über data attribute ans JS durchreichen
===========================================================================
MIGRATIONS
- rename 2.0 migration once it will be released
===========================================================================

View file

@ -8,7 +8,7 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [2.0](https://github.com/kevinpapst/kimai2/releases/tag/2.0)
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!**

View file

@ -9,9 +9,6 @@
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/maps";
@import "~bootstrap/scss/mixins";
//@import "~bootstrap/scss/normalize";
//@import "~bootstrap/scss/print";
//@import "~bootstrap/scss/scaffolding";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/grid";
@ -19,12 +16,12 @@
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/utilities";
//@import "~bootstrap/scss/responsive-utilities";
body {
font-family: $font-family-sans-serif;
&.invoice_print {
--bs-body-font-size: 13px;
background-color: #eee;
.table.no-border, .table.no-border td, .table.no-border th {
@ -35,6 +32,38 @@ body {
font-family: $font-family-sans-serif;
}
.mt-2 {
margin-top: 2em;
}
.mb-3 {
margin-bottom: 3em;
}
.pt-1 {
padding-top: 1em;
}
.pb-4 {
padding-bottom: 4em;
}
.ps-0 {
padding-left: 0;
}
.bt-1 {
border-top: 1px solid #000000;
}
.pull-right {
float: right;
}
.text-end {
text-align: right;
}
.invoice {
margin: 105px auto 30px auto;
padding: 50px 65px;
@ -48,7 +77,7 @@ body {
.page-header {
margin: 10px 0 20px 0;
font-size: 22px;
font-size: 20px;
> small {
color: #666;
@ -79,14 +108,11 @@ body {
}
.invoice-items {
margin-top: 2em;
margin-bottom: 3em;
.table {
thead th {
font-weight: bold;
border-bottom: 1px solid #ddd;
padding-bottom: 15px
padding-bottom: 10px
}
tfoot {
@ -105,7 +131,6 @@ body {
}
}
}
}
.footer {

View file

@ -1,6 +1,6 @@
{
"name": "kimai/kimai",
"license": "MIT",
"license": "AGPL-3.0-or-later",
"type": "project",
"description": "Kimai - Time Tracking",
"authors": [
@ -10,7 +10,7 @@
},
{
"name": "All contributors",
"homepage": "https://github.com/kevinpapst/kimai2/contributors"
"homepage": "https://github.com/kimai/kimai/contributors"
}
],
"require": {
@ -191,7 +191,7 @@
},
"extra": {
"branch-alias": {
"v2.x-dev": "2.0.x-dev"
"dev-main": "2.0.x-dev"
},
"symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",

103
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "16922ce13576f2e86b8a3c380fcf814a",
"content-hash": "f3b0627c043009647c1ab9c3ddea3f22",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -796,16 +796,16 @@
},
{
"name": "doctrine/doctrine-bundle",
"version": "2.8.0",
"version": "2.8.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "0421ebc069519a0f19b9c39e5dc18c359be0feab"
"reference": "fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0421ebc069519a0f19b9c39e5dc18c359be0feab",
"reference": "0421ebc069519a0f19b9c39e5dc18c359be0feab",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d",
"reference": "fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d",
"shasum": ""
},
"require": {
@ -891,7 +891,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.8.0"
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.8.1"
},
"funding": [
{
@ -907,7 +907,7 @@
"type": "tidelift"
}
],
"time": "2022-12-28T16:35:32+00:00"
"time": "2023-01-06T00:24:26+00:00"
},
{
"name": "doctrine/doctrine-migrations-bundle",
@ -1685,16 +1685,16 @@
},
{
"name": "egulias/email-validator",
"version": "3.2.4",
"version": "3.2.5",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "5f35e41eba05fdfbabd95d72f83795c835fb7ed2"
"reference": "b531a2311709443320c786feb4519cfaf94af796"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/5f35e41eba05fdfbabd95d72f83795c835fb7ed2",
"reference": "5f35e41eba05fdfbabd95d72f83795c835fb7ed2",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796",
"reference": "b531a2311709443320c786feb4519cfaf94af796",
"shasum": ""
},
"require": {
@ -1703,7 +1703,6 @@
"symfony/polyfill-intl-idn": "^1.15"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^8.5.8|^9.3.3",
"vimeo/psalm": "^4"
},
@ -1741,7 +1740,7 @@
],
"support": {
"issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/3.2.4"
"source": "https://github.com/egulias/EmailValidator/tree/3.2.5"
},
"funding": [
{
@ -1749,7 +1748,7 @@
"type": "github"
}
],
"time": "2022-12-30T14:09:25+00:00"
"time": "2023-01-02T17:26:14+00:00"
},
{
"name": "endroid/qr-code",
@ -9815,16 +9814,16 @@
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.5",
"version": "2.2.6",
"source": {
"type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
"reference": "4348a3a06651827a27d989ad1d13efec6bb49b19"
"reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/4348a3a06651827a27d989ad1d13efec6bb49b19",
"reference": "4348a3a06651827a27d989ad1d13efec6bb49b19",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c",
"reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c",
"shasum": ""
},
"require": {
@ -9862,9 +9861,9 @@
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"support": {
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.5"
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6"
},
"time": "2022-09-12T13:28:28+00:00"
"time": "2023-01-03T09:29:04+00:00"
},
{
"name": "twig/cssinliner-extra",
@ -10462,20 +10461,20 @@
},
{
"name": "zircote/swagger-php",
"version": "4.5.3",
"version": "4.5.4",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "e505bce612a86fe90f8fd50917e0848afc5d2ba8"
"reference": "09356f4d68d29bdf3254811fb2602a5d5d1788ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/e505bce612a86fe90f8fd50917e0848afc5d2ba8",
"reference": "e505bce612a86fe90f8fd50917e0848afc5d2ba8",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/09356f4d68d29bdf3254811fb2602a5d5d1788ea",
"reference": "09356f4d68d29bdf3254811fb2602a5d5d1788ea",
"shasum": ""
},
"require": {
"doctrine/annotations": "^1.7",
"doctrine/annotations": "^1.7 || ^2.0",
"ext-json": "*",
"php": ">=7.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
@ -10534,9 +10533,9 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/4.5.3"
"source": "https://github.com/zircote/swagger-php/tree/4.5.4"
},
"time": "2022-12-21T18:26:59+00:00"
"time": "2023-01-04T00:51:43+00:00"
}
],
"packages-dev": [
@ -10746,16 +10745,16 @@
},
{
"name": "doctrine/data-fixtures",
"version": "1.6.1",
"version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/data-fixtures.git",
"reference": "1a4232c15143ca3c127812d19b23a7961c41eeed"
"reference": "d52cc6d392717734fac908768a7319f8a417401a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/1a4232c15143ca3c127812d19b23a7961c41eeed",
"reference": "1a4232c15143ca3c127812d19b23a7961c41eeed",
"url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/d52cc6d392717734fac908768a7319f8a417401a",
"reference": "d52cc6d392717734fac908768a7319f8a417401a",
"shasum": ""
},
"require": {
@ -10808,7 +10807,7 @@
],
"support": {
"issues": "https://github.com/doctrine/data-fixtures/issues",
"source": "https://github.com/doctrine/data-fixtures/tree/1.6.1"
"source": "https://github.com/doctrine/data-fixtures/tree/1.6.2"
},
"funding": [
{
@ -10824,7 +10823,7 @@
"type": "tidelift"
}
],
"time": "2022-12-23T12:13:51+00:00"
"time": "2023-01-05T18:42:27+00:00"
},
{
"name": "doctrine/doctrine-fixtures-bundle",
@ -10979,16 +10978,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.13.1",
"version": "v3.13.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "78d2251dd86b49c609a0fd37c20dcf0a00aea5a7"
"reference": "3952f08a81bd3b1b15e11c3de0b6bf037faa8496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/78d2251dd86b49c609a0fd37c20dcf0a00aea5a7",
"reference": "78d2251dd86b49c609a0fd37c20dcf0a00aea5a7",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3952f08a81bd3b1b15e11c3de0b6bf037faa8496",
"reference": "3952f08a81bd3b1b15e11c3de0b6bf037faa8496",
"shasum": ""
},
"require": {
@ -11056,7 +11055,7 @@
"description": "A tool to automatically fix PHP code style",
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.13.1"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.13.2"
},
"funding": [
{
@ -11064,7 +11063,7 @@
"type": "github"
}
],
"time": "2022-12-18T00:47:22+00:00"
"time": "2023-01-02T23:53:50+00:00"
},
{
"name": "nikic/php-parser",
@ -11235,16 +11234,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.9.4",
"version": "1.9.7",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2"
"reference": "0501435cd342eac7664bd62155b1ef907fc60b6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0501435cd342eac7664bd62155b1ef907fc60b6f",
"reference": "0501435cd342eac7664bd62155b1ef907fc60b6f",
"shasum": ""
},
"require": {
@ -11274,7 +11273,7 @@
],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.9.4"
"source": "https://github.com/phpstan/phpstan/tree/1.9.7"
},
"funding": [
{
@ -11290,25 +11289,25 @@
"type": "tidelift"
}
],
"time": "2022-12-17T13:33:52+00:00"
"time": "2023-01-04T21:59:57+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
"version": "1.3.28",
"version": "1.3.29",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "8302a6a214b8cbbda8249cce6ec627033af26c12"
"reference": "4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/8302a6a214b8cbbda8249cce6ec627033af26c12",
"reference": "8302a6a214b8cbbda8249cce6ec627033af26c12",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3",
"reference": "4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.8.11"
"phpstan/phpstan": "^1.9.7"
},
"conflict": {
"doctrine/collections": "<1.0",
@ -11357,9 +11356,9 @@
"description": "Doctrine extensions for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.28"
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.29"
},
"time": "2022-12-30T21:24:11+00:00"
"time": "2023-01-04T21:51:32+00:00"
},
{
"name": "phpstan/phpstan-phpunit",

View file

@ -570,316 +570,6 @@ parameters:
count: 1
path: src/Command/InvoiceCreateCommand.php
-
message: "#^Call to an undefined method object\\:\\:getEventManager\\(\\)\\.$#"
count: 3
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getCustomers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getName\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 4
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getProjects\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getTimezone\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method hasUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) should return int but returns mixed\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) should return App\\\\Entity\\\\Activity but returns App\\\\Entity\\\\Activity\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:deactivateLifecycleCallbacks\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchIteratorFromImport\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importCustomers\\(\\) has parameter \\$customers with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownCustomer\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownGroup\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownProject\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownUser\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:prepareOptionsFromInput\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setActivityCache\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setCustomerCache\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setProjectCache\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setUserCache\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$customer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$customer of method App\\\\Entity\\\\Team\\:\\:addCustomer\\(\\) expects App\\\\Entity\\\\Customer, App\\\\Entity\\\\Customer\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\TimesheetMeta\\:\\:setName\\(\\) expects string, mixed given\\.$#"
count: 3
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$object of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:persist\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addTeamlead\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Timesheet\\:\\:setUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$object of method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateImport\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#"
count: 2
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$team of method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) expects App\\\\Entity\\\\Team, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$version2 of function version_compare expects string, mixed given\\.$#"
count: 2
path: src/Command/KimaiImporterCommand.php
-
message: "#^Property App\\\\Command\\\\KimaiImporterCommand\\:\\:\\$oldActivities type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
@ -6761,7 +6451,7 @@ parameters:
path: src/Project/ProjectStatisticService.php
-
message: "#^Strict comparison using \\!\\=\\= between null and array\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#"
message: "#^Strict comparison using \\!\\=\\= between null and list\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
@ -6970,31 +6660,6 @@ parameters:
count: 1
path: src/Repository/CustomerRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:__construct\\(\\) has parameter \\$directories with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:addDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:findByPaths\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:removeDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Cannot access offset 'counter' on mixed\\.$#"
count: 1
@ -7135,11 +6800,6 @@ parameters:
count: 1
path: src/Repository/Paginator/LoaderPaginator.php
-
message: "#^Method App\\\\Repository\\\\Paginator\\\\LoaderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/Paginator/LoaderPaginator.php
-
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) expects array, mixed given\\.$#"
count: 1
@ -7165,11 +6825,6 @@ parameters:
count: 1
path: src/Repository/Paginator/QueryBuilderPaginator.php
-
message: "#^Method App\\\\Repository\\\\Paginator\\\\QueryBuilderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/Paginator/QueryBuilderPaginator.php
-
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1

View file

@ -24,7 +24,7 @@
"/build/invoice.c8ae95ad.js"
],
"css": [
"/build/invoice.b17784c1.css"
"/build/invoice.3c80ee80.css"
]
},
"invoice-pdf": {
@ -68,7 +68,7 @@
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
"/build/invoice.b17784c1.css": "sha384-Y1e218/TMmOrl/aQcb77ix5qgFQDPPWpeD6GUtnX16Buj7/Mr6arWMSXFdqlJzAc",
"/build/invoice.3c80ee80.css": "sha384-xjFM2m/EeN7z42ygpt77ll5zAcHTeOjbZFAw2Qcu3IJutgebiAbrXjnH5aJfF5Z6",
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@
"build/app.js": "/build/app.694c6cb5.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
"build/invoice.css": "/build/invoice.b17784c1.css",
"build/invoice.css": "/build/invoice.3c80ee80.css",
"build/invoice.js": "/build/invoice.c8ae95ad.js",
"build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css",
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",

View file

@ -9,12 +9,17 @@
namespace App\API\Authentication;
use App\Entity\User;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
final class ApiTokenUpgradeBadge implements BadgeInterface
{
/**
* @param string|null $plaintextApiToken
* @param PasswordUpgraderInterface<User> $passwordUpgrader
*/
public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader)
{
}
@ -31,6 +36,9 @@ final class ApiTokenUpgradeBadge implements BadgeInterface
return $password;
}
/**
* @return PasswordUpgraderInterface<User>
*/
public function getPasswordUpgrader(): PasswordUpgraderInterface
{
return $this->passwordUpgrader;

File diff suppressed because it is too large Load diff

View file

@ -77,18 +77,18 @@ abstract class TimesheetAbstractController extends AbstractController
$table->setPaginationRoute($paginationRoute);
$table->setReloadEvents('kimai.timesheetUpdate kimai.timesheetDelete');
$table->addColumn('date', ['class' => 'alwaysVisible', 'orderBy' => 'begin']);
$table->addColumn('date', ['class' => 'alwaysVisible text-nowrap', 'orderBy' => 'begin']);
if ($this->canSeeStartEndTime()) {
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'begin']);
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'end']);
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'begin']);
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'end']);
}
$table->addColumn('duration', ['class' => 'text-end text-nowrap']);
if ($canSeeRate) {
$table->addColumn('hourlyRate', ['class' => 'text-end d-none']);
$table->addColumn('rate', ['class' => 'text-end']);
$table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']);
$table->addColumn('rate', ['class' => 'text-end text-nowrap']);
}
$table->addColumn('customer', ['class' => 'd-none d-md-table-cell']);
@ -98,7 +98,7 @@ abstract class TimesheetAbstractController extends AbstractController
$table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]);
foreach ($metaColumns as $metaColumn) {
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false]);
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
}
if ($canSeeUsername) {
@ -116,11 +116,8 @@ abstract class TimesheetAbstractController extends AbstractController
'page_setup' => $page,
'dataTable' => $table,
'action_single' => $this->getActionNameSingle(),
'canSeeUsername' => $canSeeUsername,
'canSeeRate' => $canSeeRate,
'stats' => $result->getStatistic(),
'showSummary' => $this->includeSummary(),
'showStartEndTime' => $this->canSeeStartEndTime(),
'metaColumns' => $metaColumns,
'allowMarkdown' => $this->hasMarkdownSupport(),
'editRoute' => $this->getEditRoute()

View file

@ -16,6 +16,7 @@ use App\Form\Type\SkinType;
use App\Form\Type\TimezoneType;
use App\User\UserService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -42,18 +43,17 @@ final class WizardController extends AbstractController
if ($wizard === 'profile') {
$data = [
'language' => $request->getLocale(),
'timezone' => $user->getTimezone(),
UserPreference::LOCALE => $request->getLocale(),
UserPreference::TIMEZONE => $user->getTimezone(),
UserPreference::SKIN => $user->getSkin(),
'reload' => '0',
];
$form = $this->createFormBuilder($data)
->add(UserPreference::LOCALE, LanguageType::class)
->add(UserPreference::TIMEZONE, TimezoneType::class)
->add(UserPreference::SKIN, SkinType::class, [
'attr' => [
'onchange' => "document.body.classList.remove('theme-light');document.body.classList.remove('theme-light');"
],
])
->add(UserPreference::SKIN, SkinType::class)
->add('reload', HiddenType::class)
->setAction($this->generateUrl('wizard', ['wizard' => 'profile']))
->setMethod('POST')
->getForm();
@ -69,7 +69,11 @@ final class WizardController extends AbstractController
$user->setWizardAsSeen('profile');
$userService->updateUser($user);
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
if ($data['reload'] === '1') {
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
} else {
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
}
}
return $this->render('wizard/profile.html.twig', [

View file

@ -80,9 +80,12 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
$prefs = $this->getUserPreferences($user, $userData[7]);
$user->setPreferences($prefs);
// better to be able to test the wizard in demo installations
/*
foreach (User::WIZARDS as $wizard) {
$user->setWizardAsSeen($wizard);
}
*/
$manager->persist($prefs[0]);
$manager->persist($prefs[1]);
}

View file

@ -439,6 +439,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return (bool) $this->getPreferenceValue('export_decimal', false, false);
}
public function getSkin(): string
{
return (string) $this->getPreferenceValue(UserPreference::SKIN, 'default', false);
}
public function setTimezone(?string $timezone)
{
if ($timezone === null) {

View file

@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @template-implements PasswordUpgraderInterface<User>
*/
class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface
{
public function __construct(private UserRepository $userRepository)

View file

@ -11,6 +11,7 @@ namespace App\Repository;
use App\Model\InvoiceDocument;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
final class InvoiceDocumentRepository
{
@ -21,6 +22,9 @@ final class InvoiceDocumentRepository
*/
private array $documentDirs = [];
/**
* @param array<string> $directories
*/
public function __construct(array $directories)
{
foreach ($directories as $directory) {
@ -31,23 +35,19 @@ final class InvoiceDocumentRepository
/**
* @CloudRequired
*/
public function addDirectory(string $directory)
public function addDirectory(string $directory): void
{
$this->documentDirs[] = $directory;
return $this;
}
/**
* @CloudRequired
*/
public function removeDirectory(string $directory)
public function removeDirectory(string $directory): void
{
if (($key = array_search($directory, $this->documentDirs)) !== false) {
unset($this->documentDirs[$key]);
}
return $this;
}
/**
@ -59,7 +59,12 @@ final class InvoiceDocumentRepository
throw new \InvalidArgumentException('Cannot delete built-in invoice template');
}
@unlink(realpath($invoiceDocument->getFilename()));
$realpath = realpath($invoiceDocument->getFilename());
if ($realpath === false) {
throw new \InvalidArgumentException('Template does not exist: ' . $invoiceDocument->getFilename());
}
@unlink($realpath);
}
public function getUploadDirectory(): string
@ -135,6 +140,7 @@ final class InvoiceDocumentRepository
/**
* Returns an array of invoice documents.
*
* @param array<string> $paths
* @return InvoiceDocument[]
*/
private function findByPaths(array $paths): array
@ -153,7 +159,8 @@ final class InvoiceDocumentRepository
continue;
}
$finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->name('*.*');
$finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->depth(0)->name('*.*');
/** @var SplFileInfo $file */
foreach ($finder->getIterator() as $file) {
$doc = new InvoiceDocument($file);
// the first found invoice document wins

View file

@ -24,6 +24,9 @@ final class LoaderPaginator implements PaginatorInterface
return $this->results;
}
/**
* @return iterable<array-key, iterable<mixed>>
*/
public function getSlice(int $offset, int $length): iterable
{
$query = $this->query
@ -34,13 +37,17 @@ final class LoaderPaginator implements PaginatorInterface
return $this->getResults($query);
}
/**
* @param Query<null, mixed> $query
* @return iterable<array-key, iterable<mixed>>
*/
private function getResults(Query $query)
{
$results = $query->execute();
$this->loader->loadResults($results);
return $results;
return $results; // @phpstan-ignore-line
}
public function getAll(): iterable

View file

@ -23,6 +23,9 @@ final class QueryBuilderPaginator implements PaginatorInterface
return $this->results;
}
/**
* @return iterable<array-key, iterable<mixed>>
*/
public function getSlice(int $offset, int $length): iterable
{
$query = $this->query
@ -33,9 +36,13 @@ final class QueryBuilderPaginator implements PaginatorInterface
return $this->getResults($query);
}
/**
* @param Query<null, mixed> $query
* @return iterable<array-key, iterable<mixed>>
*/
private function getResults(Query $query)
{
return $query->execute();
return $query->execute(); // @phpstan-ignore-line
}
public function getAll(): iterable

View file

@ -34,6 +34,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @extends \Doctrine\ORM\EntityRepository<User>
* @template-implements PasswordUpgraderInterface<User>
*/
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
{
@ -58,7 +59,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
$entityManager->flush();
}
public function upgradePassword(PasswordAuthenticatedUserInterface|UserInterface $user, string $newHashedPassword): void
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!($user instanceof User)) {
return;

View file

@ -10,6 +10,7 @@
namespace App\Security;
use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Ldap\LdapUserProvider;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@ -17,6 +18,9 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @template-implements PasswordUpgraderInterface<User>
*/
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
private ?ChainUserProvider $provider = null;

View file

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig\Runtime;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\Writer\PngWriter;
use Twig\Extension\RuntimeExtensionInterface;
final class QrCodeExtension implements RuntimeExtensionInterface
{
public function __construct()
{
}
/**
* @param string $data
* @param array<string, mixed> $writerOptions
* @return string
*/
public function qrCodeDataUriFunction(string $data, array $writerOptions = []): string
{
return Builder::create()
->writer(new PngWriter())
->writerOptions($writerOptions)
->data($data)
// if this causes errors at some point and needs to be configurable, keep this default!
->errorCorrectionLevel(new ErrorCorrectionLevelMedium())
->build()
->getDataUri();
}
}

View file

@ -11,6 +11,7 @@ namespace App\Twig;
use App\Twig\Runtime\EncoreExtension;
use App\Twig\Runtime\MarkdownExtension;
use App\Twig\Runtime\QrCodeExtension;
use App\Twig\Runtime\ThemeExtension;
use App\Twig\Runtime\TimesheetExtension;
use App\Twig\Runtime\WidgetExtension;
@ -35,6 +36,7 @@ final class RuntimeExtensions extends AbstractExtension
new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']),
new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFunction('icon', [RuntimeExtension::class, 'createIcon'], ['is_safe' => ['html']]),
new TwigFunction('qr_code_data_uri', [QrCodeExtension::class, 'qrCodeDataUriFunction']),
];
}

View file

@ -27,19 +27,21 @@
{% block datatable_before %}{% endblock %}
{% set sortedColumns = dataTable.sortedColumnNames %}
{% for entry in dataTable %}
{% block datatable_row %}
<tr{% block datatable_row_attr %}{% endblock %}>
{% for column, data in sortedColumns %}
{% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}>
{% block datatable_column_value %}{% endblock %}
</td>
{% endblock %}
{% endfor %}
</tr>
{% endblock %}
{% endfor %}
{% block datatable_outer %}
{% for entry in dataTable %}
{% block datatable_row %}
<tr{% block datatable_row_attr %}{% endblock %}>
{% for column, data in sortedColumns %}
{% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}>
{% block datatable_column_value %}{% endblock %}
</td>
{% endblock %}
{% endfor %}
</tr>
{% endblock %}
{% endfor %}
{% endblock %}
{% block datatable_after %}{% endblock %}

View file

@ -62,7 +62,7 @@
<div class="col-sm-7"></div>
</div>
<div class="row invoice-items">
<div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive">
<table class="table">
<thead>

View file

@ -15,9 +15,9 @@
<div class="row">
<div class="col-xs-12">
<table class="table no-border table-sm">
<table class="table no-border table-sm">
<tr>
<th>{{ 'invoice.from'|trans }}</th>
<th class="ps-0">{{ 'invoice.from'|trans }}</th>
<td contenteditable="true">
{% if model.query.user is not empty %}
{{ widgets.username(model.query.user) }}
@ -27,7 +27,7 @@
</td>
</tr>
<tr>
<th>{{ 'date'|trans }}</th>
<th class="ps-0">{{ 'date'|trans }}</th>
<td contenteditable="true">
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %}
{{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
@ -37,7 +37,7 @@
</td>
</tr>
<tr>
<th>{{ 'customer'|trans }}</th>
<th class="ps-0">{{ 'customer'|trans }}</th>
<td contenteditable="true">
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
@ -45,7 +45,7 @@
</tr>
{% if project is not null %}
<tr>
<th>{{ 'project'|trans }}</th>
<th class="ps-0">{{ 'project'|trans }}</th>
<td contenteditable="true">
{{ project.name }}
{% if project.orderNumber is not empty %}
@ -56,7 +56,7 @@
{% endif %}
{% if activity is not null %}
<tr>
<th>{{ 'activity'|trans }}</th>
<th class="ps-0">{{ 'activity'|trans }}</th>
<td contenteditable="true">
{{ activity.name }}
</td>
@ -66,7 +66,7 @@
</div>
</div>
<div class="row invoice-items">
<div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive">
<table class="table table-striped">
<thead>
@ -122,16 +122,8 @@
{% endif %}
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
</tr>
<tr>
<th>{{ 'invoice.signature_customer'|trans }}</th>
</tr>
</tbody>
</table>
<p class="bt-1 pb-4 pt-1">{{ 'invoice.signature_user'|trans }}</p>
<p class="bt-1 pb-4 pt-1">{{ 'invoice.signature_customer'|trans }}</p>
</div>
</div>
</div>

View file

@ -2,44 +2,68 @@
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% set checkOverlappingDesc = false %}
{% set checkOverlappingAsc = false %}
{% set query = dataTable.getQuery() %}
{% if query.orderBy == 'begin' or query.orderBy == 'end' %}
{% set checkOverlappingDesc = (query.order == 'DESC') %}
{% set checkOverlappingAsc = not checkOverlappingDesc %}
{% endif %}
{% block datatable_outer %}
{% set checkOverlappingDesc = false %}
{% set checkOverlappingAsc = false %}
{% set query = dataTable.getQuery() %}
{% if query.orderBy == 'begin' or query.orderBy == 'end' %}
{% set checkOverlappingDesc = (query.order == 'DESC') %}
{% set checkOverlappingAsc = not checkOverlappingDesc %}
{% endif %}
{% set day = null %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{% set lastEntry = null %}
{% set day = null %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{% set lastEntry = null %}
{% for entry in dataTable %}
{%- if day is same as(null) -%}
{% set day = entry.begin|date_short %}
{% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }}
{% set day = entry.begin|date_short %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{%- endif -%}
{%- set customerCurrency = entry.project.customer.currency -%}
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
{% block datatable_row %}
<tr{{ block('datatable_row_attr') }}>
{% for column, data in sortedColumns %}
{{ block('datatable_column') }}
{% endfor %}
</tr>
{% endblock %}
{%- if entry.end -%}
{% if dayRate[customerCurrency] is not defined %}
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
{% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
{%- endif -%}
{% if dayHourlyRate is not null %}
{% if dayHourlyRate == 0 %}
{% set dayHourlyRate = entryHourlyRate %}
{% elseif dayHourlyRate != entryHourlyRate %}
{% set dayHourlyRate = null %}
{% endif %}
{% endif %}
{%- set dayDuration = dayDuration + entry.duration -%}
{% set lastEntry = entry %}
{% endfor %}
{% if showSummary %}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }}
{% endif %}
{% endblock %}
{% block status %}
{% from "macros/status.html.twig" import status_duration %}
{{ status_duration(stats.duration|duration) }}
{% endblock %}
{% block datatable_after %}
{% if showSummary %}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% endif %}
{% endblock %}
{% block datatable_row %}
{%- set customerCurrency = entry.project.customer.currency -%}
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
{%- if day is same as(null) -%}
{% set day = entry.begin|date_short %}
{% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% set day = entry.begin|date_short %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{%- endif -%}
{% block datatable_row_attr %}
{% set class = '' %}
{% if checkOverlappingDesc or checkOverlappingAsc %}
{% if lastEntry is not null and entry.end is not null and entry.user is same as (lastEntry.user) %}
@ -53,144 +77,94 @@
{% if not entry.end %}
{% set class = class ~ ' recording' %}
{% endif %}
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{{ class }}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}>
<td class="text-nowrap">
{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{{ class }}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}
{% endblock %}
{% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}{% if column == 'description' %} timesheet-description{% endif %}">
{% if column == 'id' %}
{% if is_granted('edit', entry) or is_granted('delete', entry) %}
{{ tables.datatable_multiupdate_row(entry.id) }}
{% endif %}
</td>
<td class="text-nowrap {{ tables.class(dataTable, 'date') }}">{{ entry.begin|date_short }}</td>
{% if showStartEndTime %}
<td class="text-nowrap {{ tables.class(dataTable, 'starttime') }}">{{ entry.begin|time }}</td>
<td class="text-nowrap {{ tables.class(dataTable, 'endtime') }}">
{% if entry.end %}
{{ entry.end|time }}
{% else %}
&dash;
{% endif %}
</td>
{% endif %}
{% elseif column == 'date' %}
{{ entry.begin|date_short }}
{% elseif column == 'starttime' %}
{{ entry.begin|time }}
{% elseif column == 'endtime' %}
{% if entry.end %}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}">{{ entry.duration|duration }}</td>
{{ entry.end|time }}
{% else %}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}">
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</i>
</td>
&dash;
{% endif %}
{% if canSeeRate %}
<td class="text-nowrap {{ tables.class(dataTable, 'hourlyRate') }}">
{{ entryHourlyRate }}
</td>
<td class="text-nowrap {{ tables.class(dataTable, 'rate') }}">
{% if not entry.end or not is_granted('view_rate', entry) %}
&dash;
{% else %}
{{ entry.rate|money(customerCurrency) }}
{% endif %}
</td>
{% elseif column == 'duration' %}
{% if entry.end %}
{{ entry.duration|duration }}
{% else %}
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</i>
{% endif %}
<td class="{{ tables.class(dataTable, 'customer') }}">
{{ widgets.label_customer(entry.project.customer) }}
</td>
<td class="{{ tables.class(dataTable, 'project') }}">
{{ widgets.label_project(entry.project) }}
</td>
<td class="{{ tables.class(dataTable, 'activity') }}">
{% if entry.activity is not null %}
{{ widgets.label_activity(entry.activity) }}
{% endif %}
</td>
<td class="{{ tables.class(dataTable, 'description') }} timesheet-description">
{% if allowMarkdown %}
{{ entry.description|desc2html }}
{% else %}
{{ entry.description|nl2br }}
{% endif %}
</td>
<td class="{{ tables.class(dataTable, 'tags') }}">{{ widgets.tag_list(entry.tags) }}</td>
{% for field in metaColumns %}
<td class="{{ tables.class(dataTable, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
{% if canSeeUsername %}
<td class="{{ tables.class(dataTable, 'username') }}">
{{ widgets.label_user(entry.user) }}
</td>
{% elseif column == 'hourlyRate' %}
{{ entryHourlyRate }}
{% elseif column == 'rate' %}
{% if not entry.end or not is_granted('view_rate', entry) %}
&dash;
{% else %}
{{ entry.rate|money(customerCurrency) }}
{% endif %}
<td class="{{ tables.class(dataTable, 'billable') }}">
{{ widgets.label_boolean(entry.billable) }}
</td>
<td class="{{ tables.class(dataTable, 'exported') }}">
{{ widgets.label_boolean(entry.exported) }}
</td>
<td class="{{ tables.class(dataTable, 'actions') }}">
{% set event = actions(app.user, action_single, 'index', {'timesheet': entry}) %}
{{ widgets.table_actions(event.actions) }}
</td>
</tr>
{%- if entry.end -%}
{% if dayRate[customerCurrency] is not defined %}
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
{% elseif column == 'customer' %}
{{ widgets.label_customer(entry.project.customer) }}
{% elseif column == 'project' %}
{{ widgets.label_project(entry.project) }}
{% elseif column == 'activity' %}
{% if entry.activity is not null %}
{{ widgets.label_activity(entry.activity) }}
{% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
{%- endif -%}
{% if dayHourlyRate is not null %}
{% if dayHourlyRate == 0 %}
{% set dayHourlyRate = entryHourlyRate %}
{% elseif dayHourlyRate != entryHourlyRate %}
{% set dayHourlyRate = null %}
{% elseif column == 'description' %}
{% if allowMarkdown %}
{{ entry.description|desc2html }}
{% else %}
{{ entry.description|nl2br }}
{% endif %}
{% elseif column == 'tags' %}
{{ widgets.tag_list(entry.tags) }}
{% elseif column == 'billable' %}
{{ widgets.label_boolean(entry.billable) }}
{% elseif column == 'exported' %}
{{ widgets.label_boolean(entry.exported) }}
{% elseif column == 'username' %}
{{ widgets.label_user(entry.user) }}
{% elseif column == 'actions' %}
{% set event = actions(app.user, action_single, 'index', {'timesheet': entry}) %}
{{ widgets.table_actions(event.actions) }}
{% elseif column starts with 'mf_' %}
{{ widgets.meta_field_value(entry, data) }}
{% endif %}
{%- set dayDuration = dayDuration + entry.duration -%}
{% set lastEntry = entry %}
</td>
{% endblock %}
{% macro summary(day, duration, dayHourlyRate, dayRates, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) %}
{% macro summary(day, duration, dayHourlyRate, dayRates, sortedColumns, dataTable) %}
{% import "macros/datatables.html.twig" as tables %}
<tr class="summary info">
<td></td>
<td class="text-nowrap">{{ day }}</td>
{% if showStartEndTime %}
<td class="{{ tables.class(dataTable, 'starttime') }}"></td>
<td class="{{ tables.class(dataTable, 'endtime') }}"></td>
{% endif %}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}">{{ duration|duration }}</td>
{% if canSeeRate %}
<td class="text-nowrap {{ tables.class(dataTable, 'hourlyRate') }}">
{% if dayHourlyRate is not null and dayHourlyRate != 0 %}
{{ dayHourlyRate }}
{% for column, data in sortedColumns %}
<td class="{{ tables.class(dataTable, column) }}">
{% if column == 'date' %}
{{ day }}
{% elseif column == 'duration' %}
{{ duration|duration }}
{% elseif column == 'hourlyRate' %}
{% if dayHourlyRate is not null and dayHourlyRate != 0 %}
{{ dayHourlyRate }}
{% endif %}
{% elseif column == 'rate' %}
{% for currency, rate in dayRates %}
{{ rate|money(currency) }}
{% if not loop.last %}
<br>
{% endif %}
</td>
<td class="text-nowrap {{ tables.class(dataTable, 'rate') }}">
{% for currency, rate in dayRates %}
{{ rate|money(currency) }}
{% if not loop.last %}
<br>
{% endif %}
{% endfor %}
</td>
{% endfor %}
{% else %}
{% endif %}
<td class="{{ tables.class(dataTable, 'customer') }}"></td>
<td class="{{ tables.class(dataTable, 'project') }}"></td>
<td class="{{ tables.class(dataTable, 'activity') }}"></td>
<td class="{{ tables.class(dataTable, 'description') }}"></td>
<td class="{{ tables.class(dataTable, 'tags') }}"></td>
{% for field in metaColumns %}
<td class="{{ tables.class(dataTable, 'mf_' ~ field.name) }}"></td>
{% endfor %}
{% if canSeeUsername %}
<td class="{{ tables.class(dataTable, 'username') }}"></td>
{% endif %}
<td class="{{ tables.class(dataTable, 'billable') }}"></td>
<td class="{{ tables.class(dataTable, 'exported') }}"></td>
<td class="actions"></td>
</td>
{% endfor %}
</tr>
{% endmacro %}

View file

@ -25,6 +25,14 @@
document.body.classList.add('theme-' + skinChooser.value);
});
}
const localeChooser = document.getElementById('form_language');
if (localeChooser !== null) {
localeChooser.addEventListener('change', (ev) => {
document.getElementById('form_reload').value = '1';
ev.target.form.submit();
});
}
});
</script>
{% endblock %}

View file

@ -99,7 +99,7 @@ class CreateUserCommandTest extends KernelTestCase
$commandTester = $this->createUser('MyTestUser2', 'user@example.com', 'ROLE_USER', 'foobar');
$output = $commandTester->getDisplay();
$this->assertStringContainsString('[ERROR] email: The email is already used.', $output);
$this->assertStringContainsString('[ERROR] email: This e-mail address is already in use.', $output);
}
public function testUserEmail(): void

View file

@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\KimaiImporterCommand;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @covers \App\Command\KimaiImporterCommand
* @group integration
*/
class KimaiImporterCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
parent::setUp();
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$encoder = $this->createMock(UserPasswordHasherInterface::class);
$registry = $this->createMock(ManagerRegistry::class);
$validator = $this->createMock(ValidatorInterface::class);
$this->application->add(new KimaiImporterCommand($encoder, $registry, $validator));
}
public function testCommandName()
{
$command = $this->application->find('kimai:import-v1');
self::assertInstanceOf(KimaiImporterCommand::class, $command);
}
}

View file

@ -116,7 +116,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
$content = $client->getResponse()->getContent();
$this->assertStringContainsString('<title>Kimai Time Tracking</title>', $content);
$this->assertStringContainsString('An email has been sent to register@example.com. It contains an activation link you must click to activate your account.', $content);
$this->assertStringContainsString('An e-mail has been sent to register@example.com. It contains a link you must click to activate your account.', $content);
$this->assertStringContainsString('<a href="/en/login">', $content);
}

View file

@ -5,8 +5,7 @@
"type": "kimai-plugin",
"version": "1.0",
"require": {
"kimai/kimai2-composer": "*",
"kevinpapst/kimai2": "*"
"kimai/kimai": "*"
},
"keywords": [
"kimai",

View file

@ -49,6 +49,7 @@ class RuntimeExtensionsTest extends TestCase
'encore_entry_css_source',
'render_widget',
'icon',
'qr_code_data_uri',
];
$i = 0;

View file

@ -2302,11 +2302,6 @@ parameters:
count: 1
path: Command/InvoiceCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\KimaiImporterCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/KimaiImporterCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1