mirror of
https://github.com/kevinpapst/kimai2.git
synced 2025-03-17 06:22:38 +00:00
prepare release 1.13 (#2290)
* make voters a final class * upgrade dependencies * sort project alphabetically in dashboard widget * open detail page on row click * do not break on null tag name * added max height to scrollable widgets on dashboard * added timesheet duplicate event * allow to deactivate browser title update * improve comment box * moved role permissions to own menu * removed tabs in user screen * fix user can remove super-admin from own account
This commit is contained in:
parent
a034b3519e
commit
8d41fa20bd
59 changed files with 693 additions and 232 deletions
.github/workflows
.github_changelog_generatorCHANGELOG.mdSECURITY.mdassets
composer.jsoncomposer.lockconfig/packages
public/build
src
templates
tests
Controller
CustomerControllerTest.phpPermissionControllerTest.phpProfileControllerTest.phpProjectControllerTest.phpUserControllerTest.php
Entity
Event
EventSubscriber
Utils
translations
yarn.lock
2
.github/workflows/linting.yaml
vendored
2
.github/workflows/linting.yaml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
|||
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, zip, gd, ldap
|
||||
tools: cs2pr:1.1.0
|
||||
- run: composer install --no-progress
|
||||
- run: composer validate --no-check-all --strict
|
||||
- run: composer validate --strict
|
||||
- run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php_cs.dist --using-cache=no --show-progress=none --format=checkstyle | cs2pr
|
||||
- run: vendor/bin/phpstan analyse src -c phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
|
||||
- run: vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
unreleased=true
|
||||
future-release=1.12
|
||||
future-release=1.13
|
||||
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
|
||||
issues-wo-labels=false
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## [1.13](https://github.com/kevinpapst/kimai2/tree/1.13)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13)
|
||||
|
||||
## [1.12](https://github.com/kevinpapst/kimai2/tree/1.12)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.11.1...1.12)
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ As announced in the [README](README.md) I only support the latest available rele
|
|||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| master | :white_check_mark: |
|
||||
| 1.12 | :white_check_mark: |
|
||||
| < 1.12 | :x: |
|
||||
| 1.13 | :white_check_mark: |
|
||||
| < 1.13 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
|
|||
}
|
||||
|
||||
init() {
|
||||
this.updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
|
||||
this.updateRecords();
|
||||
const self = this;
|
||||
const handle = function() { self.updateRecords(); };
|
||||
|
@ -41,7 +42,9 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
|
|||
const activeRecords = document.querySelectorAll(this.selector);
|
||||
|
||||
if (activeRecords.length === 0) {
|
||||
document.title = document.querySelector('body').dataset['title'];
|
||||
if (this.updateBrowserTitle) {
|
||||
document.title = document.querySelector('body').dataset['title'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -59,6 +62,10 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this.updateBrowserTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = durations.shift();
|
||||
let prefix = ' | ';
|
||||
|
||||
|
|
|
@ -13,4 +13,8 @@ section {
|
|||
}
|
||||
}
|
||||
}
|
||||
.box-body-scrollable {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"nelmio/cors-bundle": "^1.5",
|
||||
"onelogin/php-saml": "^3.4",
|
||||
"pagerfanta/pagerfanta": "^2.1",
|
||||
"phpoffice/phpspreadsheet": "^1.10",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"phpoffice/phpword": "^0.17",
|
||||
"psr/log": "^1.1",
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
|
@ -66,6 +66,7 @@
|
|||
"symfony/serializer": "^4.4",
|
||||
"symfony/translation": "^4.4",
|
||||
"symfony/twig-bundle": "^4.4",
|
||||
"symfony/uid": "^5.0",
|
||||
"symfony/validator": "^4.4",
|
||||
"symfony/webpack-encore-bundle": "^1.5",
|
||||
"symfony/yaml": "^4.4",
|
||||
|
@ -132,7 +133,6 @@
|
|||
"symfony/polyfill-php54": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring",
|
||||
"laminas/laminas-ldap": "For LDAP authentication with Kimai"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
254
composer.lock
generated
254
composer.lock
generated
|
@ -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": "a0d2193a20b2d2620cda4d347f5160f0",
|
||||
"content-hash": "7931f435fc1cfba7a09fe9d65369984e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/doctrineextensions",
|
||||
|
@ -1680,6 +1680,60 @@
|
|||
"description": "A php library to manipulate Swagger specifications",
|
||||
"time": "2018-07-27T06:40:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
"version": "v4.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
|
||||
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"HTMLPurifier": "library/"
|
||||
},
|
||||
"files": [
|
||||
"library/HTMLPurifier.composer.php"
|
||||
],
|
||||
"exclude-from-classmap": [
|
||||
"/library/HTMLPurifier/Language/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Edward Z. Yang",
|
||||
"email": "admin@htmlpurifier.org",
|
||||
"homepage": "http://ezyang.com"
|
||||
}
|
||||
],
|
||||
"description": "Standards compliant HTML filter written in PHP",
|
||||
"homepage": "http://htmlpurifier.org/",
|
||||
"keywords": [
|
||||
"html"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/master"
|
||||
},
|
||||
"time": "2020-06-29T00:56:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "friendsofsymfony/rest-bundle",
|
||||
"version": "3.0.3",
|
||||
|
@ -3375,16 +3429,16 @@
|
|||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
"version": "1.7.6",
|
||||
"version": "1.7.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/php-enum.git",
|
||||
"reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c"
|
||||
"reference": "d178027d1e679832db9f38248fcc7200647dc2b7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/5f36467c7a87e20fbdc51e524fd8f9d1de80187c",
|
||||
"reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c",
|
||||
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/d178027d1e679832db9f38248fcc7200647dc2b7",
|
||||
"reference": "d178027d1e679832db9f38248fcc7200647dc2b7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -3417,7 +3471,21 @@
|
|||
"keywords": [
|
||||
"enum"
|
||||
],
|
||||
"time": "2020-02-14T08:15:52+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/php-enum/issues",
|
||||
"source": "https://github.com/myclabs/php-enum/tree/1.7.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/mnapoli",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-11-14T18:14:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/api-doc-bundle",
|
||||
|
@ -4151,16 +4219,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f"
|
||||
"reference": "76d4323b85129d0c368149c831a07a3e258b2b50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f",
|
||||
"reference": "a8e8068b31b8119e1daa5b1eb5715a3a8ea8305f",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/76d4323b85129d0c368149c831a07a3e258b2b50",
|
||||
"reference": "76d4323b85129d0c368149c831a07a3e258b2b50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -4177,10 +4245,11 @@
|
|||
"ext-xmlwriter": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ezyang/htmlpurifier": "^4.13",
|
||||
"maennchen/zipstream-php": "^2.1",
|
||||
"markbaker/complex": "^1.5|^2.0",
|
||||
"markbaker/matrix": "^1.2|^2.0",
|
||||
"php": "^7.2|^8.0",
|
||||
"markbaker/complex": "^1.5||^2.0",
|
||||
"markbaker/matrix": "^1.2||^2.0",
|
||||
"php": "^7.2||^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0"
|
||||
|
@ -4191,7 +4260,7 @@
|
|||
"jpgraph/jpgraph": "^4.0",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpunit/phpunit": "^8.5|^9.3",
|
||||
"phpunit/phpunit": "^8.5||^9.3",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.3"
|
||||
},
|
||||
|
@ -4243,7 +4312,11 @@
|
|||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"time": "2020-10-11T13:20:59+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.16.0"
|
||||
},
|
||||
"time": "2020-12-31T18:03:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
|
@ -8078,6 +8151,85 @@
|
|||
],
|
||||
"time": "2020-10-23T14:02:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-uuid",
|
||||
"version": "v1.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-uuid.git",
|
||||
"reference": "17e0611d2e180a91d02b4fa8b03aab0368b661bc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/17e0611d2e180a91d02b4fa8b03aab0368b661bc",
|
||||
"reference": "17e0611d2e180a91d02b4fa8b03aab0368b661bc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-uuid": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.22-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Uuid\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Grégoire Pineau",
|
||||
"email": "lyrixx@lyrixx.info"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for uuid functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"uuid"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.22.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-01-07T16:49:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/postmark-mailer",
|
||||
"version": "v4.4.16",
|
||||
|
@ -9490,6 +9642,76 @@
|
|||
],
|
||||
"time": "2020-10-24T11:50:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/uid",
|
||||
"version": "v5.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/uid.git",
|
||||
"reference": "7085124d58b662d3fdfb1f7d2dde6c5659656aa4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/uid/zipball/7085124d58b662d3fdfb1f7d2dde6c5659656aa4",
|
||||
"reference": "7085124d58b662d3fdfb1f7d2dde6c5659656aa4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-uuid": "^1.15"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Uid\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Grégoire Pineau",
|
||||
"email": "lyrixx@lyrixx.info"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Uid component",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"UID",
|
||||
"uuid"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/uid/tree/v5.2.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-12-15T09:12:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/validator",
|
||||
"version": "v4.4.16",
|
||||
|
@ -13343,5 +13565,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.2.9"
|
||||
},
|
||||
"plugin-api-version": "1.1.0"
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ doctrine:
|
|||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
# existing migrations will fail if the schema filter is activated
|
||||
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
# this setting prevents automatic database detection and finds a lot of false-negatives on doctrine:migrations:diff
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,10 +5,10 @@
|
|||
"build/runtime.098eaae1.js",
|
||||
"build/0.79dbdbb9.js",
|
||||
"build/1.32489d92.js",
|
||||
"build/app.4f01ca95.js"
|
||||
"build/app.19a715ed.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.856a8108.css"
|
||||
"build/app.cafebaaa.css"
|
||||
]
|
||||
},
|
||||
"invoice": {
|
||||
|
@ -53,8 +53,8 @@
|
|||
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
||||
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
||||
"build/app.4f01ca95.js": "sha384-4gnIIZ7TRMm2k6gumDyHoJMiNcEtr7Swtd0i8s8HAiwExFTqb0hrAhP7s4I7kjim",
|
||||
"build/app.856a8108.css": "sha384-LXQ3xtGnzZrgD1R/P0zS2d34VL7+QYCeQvjV0hzFHeoM/IppVbIUAt2FzuORmCuj",
|
||||
"build/app.19a715ed.js": "sha384-Pesgkybq559ztW96CLUqdAiQq6IE19D/qMe/S3YdCsUA2PeaMBV4vYqIrXqDD9Gd",
|
||||
"build/app.cafebaaa.css": "sha384-gTa4tNAiWKmWXUNGKUz45SW9E2Etr68bq3XC+Y+a+VC5EOfB3s9pbNGlwp6yEinW",
|
||||
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||
"build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"build/0.79dbdbb9.js": "build/0.79dbdbb9.js",
|
||||
"build/1.32489d92.js": "build/1.32489d92.js",
|
||||
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
||||
"build/app.css": "build/app.856a8108.css",
|
||||
"build/app.js": "build/app.4f01ca95.js",
|
||||
"build/app.css": "build/app.cafebaaa.css",
|
||||
"build/app.js": "build/app.19a715ed.js",
|
||||
"build/calendar.css": "build/calendar.1408f57e.css",
|
||||
"build/calendar.js": "build/calendar.070aab88.js",
|
||||
"build/chart.js": "build/chart.34d60a88.js",
|
||||
|
|
|
@ -13,6 +13,8 @@ namespace App\API;
|
|||
|
||||
use App\Entity\User;
|
||||
use App\Event\RecentActivityEvent;
|
||||
use App\Event\TimesheetDuplicatePostEvent;
|
||||
use App\Event\TimesheetDuplicatePreEvent;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
use App\Form\API\TimesheetApiEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
@ -727,8 +729,12 @@ class TimesheetController extends BaseApiController
|
|||
|
||||
$copyTimesheet = clone $timesheet;
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
|
||||
|
||||
$this->service->saveNewTimesheet($copyTimesheet);
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
|
||||
|
||||
$view = new View($copyTimesheet, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class Constants
|
|||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.12';
|
||||
public const VERSION = '1.13';
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
|
|
|
@ -25,7 +25,6 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
|||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
|
@ -35,7 +34,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
|||
* @Route(path="/profile")
|
||||
* @Security("is_granted('view_own_profile') or is_granted('view_other_profile')")
|
||||
*/
|
||||
class ProfileController extends AbstractController
|
||||
final class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
|
@ -50,11 +49,10 @@ class ProfileController extends AbstractController
|
|||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher, TeamRepository $teams)
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->teams = $teams;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,7 +102,11 @@ class ProfileController extends AbstractController
|
|||
return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'settings', $form);
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => 'settings',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,7 +131,11 @@ class ProfileController extends AbstractController
|
|||
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'password', null, $form);
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => 'password',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,7 +160,11 @@ class ProfileController extends AbstractController
|
|||
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'api-token', null, null, null, $form);
|
||||
return $this->render('user/api-token.html.twig', [
|
||||
'tab' => 'api-token',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,10 +173,18 @@ class ProfileController extends AbstractController
|
|||
*/
|
||||
public function rolesAction(User $profile, Request $request)
|
||||
{
|
||||
$isSuperAdmin = $profile->isSuperAdmin();
|
||||
|
||||
$form = $this->createRolesForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// fix that a super admin cannot remove this role from himself.
|
||||
// would be a massive problem, in case that there is only one super-admin account existing
|
||||
if ($isSuperAdmin && !$profile->isSuperAdmin() && $profile->getId() === $this->getUser()->getId()) {
|
||||
$profile->setSuperAdmin(true);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
@ -176,7 +194,11 @@ class ProfileController extends AbstractController
|
|||
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'roles', null, null, $form);
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => 'roles',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -198,7 +220,11 @@ class ProfileController extends AbstractController
|
|||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'teams', null, null, null, null, $form);
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => 'teams',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -283,45 +309,6 @@ class ProfileController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
protected function getProfileView(
|
||||
User $user,
|
||||
string $tab,
|
||||
FormInterface $editForm = null,
|
||||
FormInterface $pwdForm = null,
|
||||
FormInterface $rolesForm = null,
|
||||
FormInterface $apiTokenForm = null,
|
||||
FormInterface $teamsForm = null
|
||||
): Response {
|
||||
$forms = [];
|
||||
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$editForm = $editForm ?: $this->createEditForm($user);
|
||||
$forms['settings'] = $editForm->createView();
|
||||
}
|
||||
if ($this->isGranted('password', $user)) {
|
||||
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
|
||||
$forms['password'] = $pwdForm->createView();
|
||||
}
|
||||
if ($this->isGranted('api-token', $user)) {
|
||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||
$forms['api-token'] = $apiTokenForm->createView();
|
||||
}
|
||||
if ($this->isGranted('teams', $user) && $this->teams->count([]) > 0) {
|
||||
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
|
||||
$forms['teams'] = $teamsForm->createView();
|
||||
}
|
||||
if ($this->isGranted('roles', $user)) {
|
||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||
$forms['roles'] = $rolesForm->createView();
|
||||
}
|
||||
|
||||
return $this->render('user/profile.html.twig', [
|
||||
'tab' => $tab,
|
||||
'user' => $user,
|
||||
'forms' => $forms
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPreferencesForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
|
|
|
@ -51,7 +51,7 @@ class Tag
|
|||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=100, allowEmptyString=false)
|
||||
* @Assert\Length(min=2, max=100, allowEmptyString=false, normalizer="trim")
|
||||
* @Assert\Regex(pattern="/,/",match=false,message="Tag name cannot contain comma")
|
||||
*/
|
||||
private $name;
|
||||
|
@ -77,7 +77,7 @@ class Tag
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $tagName): Tag
|
||||
public function setName(?string $tagName): Tag
|
||||
{
|
||||
$this->name = $tagName;
|
||||
|
||||
|
|
|
@ -439,6 +439,18 @@ class User extends BaseUser implements UserInterface
|
|||
return !$this->getTeams()->isEmpty();
|
||||
}
|
||||
|
||||
public function hasTeamMember(User $user): bool
|
||||
{
|
||||
/** @var Team $team */
|
||||
foreach ($this->getTeams() as $team) {
|
||||
if ($team->hasUser($user)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
|
|
31
src/Event/TimesheetDuplicatePostEvent.php
Normal file
31
src/Event/TimesheetDuplicatePostEvent.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class TimesheetDuplicatePostEvent extends AbstractTimesheetEvent
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
private $original;
|
||||
|
||||
public function __construct(Timesheet $new, Timesheet $original)
|
||||
{
|
||||
parent::__construct($new);
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
public function getOriginalTimesheet(): Timesheet
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
}
|
31
src/Event/TimesheetDuplicatePreEvent.php
Normal file
31
src/Event/TimesheetDuplicatePreEvent.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
final class TimesheetDuplicatePreEvent extends AbstractTimesheetEvent
|
||||
{
|
||||
/**
|
||||
* @var Timesheet
|
||||
*/
|
||||
private $original;
|
||||
|
||||
public function __construct(Timesheet $new, Timesheet $original)
|
||||
{
|
||||
parent::__construct($new);
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
public function getOriginalTimesheet(): Timesheet
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
}
|
|
@ -130,7 +130,12 @@ final class MenuSubscriber implements EventSubscriberInterface
|
|||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$users = new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], $this->getIcon('user'));
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'admin_user_permissions', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences']);
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences']);
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('role_permissions')) {
|
||||
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], $this->getIcon('permissions'));
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
|
|
|
@ -159,6 +159,13 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||
->setSection('theme')
|
||||
->setType(CheckboxType::class),
|
||||
|
||||
(new UserPreference())
|
||||
->setName('theme.update_browser_title')
|
||||
->setValue(true)
|
||||
->setOrder(550)
|
||||
->setSection('theme')
|
||||
->setType(CheckboxType::class),
|
||||
|
||||
(new UserPreference())
|
||||
->setName('calendar.initial_view')
|
||||
->setValue(CalendarViewType::DEFAULT_VIEW)
|
||||
|
|
|
@ -21,9 +21,6 @@ class TagArrayToStringTransformer implements DataTransformerInterface
|
|||
*/
|
||||
private $tagRepository;
|
||||
|
||||
/**
|
||||
* @param TagRepository $tagRepository
|
||||
*/
|
||||
public function __construct(TagRepository $tagRepository)
|
||||
{
|
||||
$this->tagRepository = $tagRepository;
|
||||
|
@ -33,7 +30,6 @@ class TagArrayToStringTransformer implements DataTransformerInterface
|
|||
* Transforms an array of tags to a string.
|
||||
*
|
||||
* @param Tag[]|null $tags
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function transform($tags)
|
||||
|
@ -48,35 +44,34 @@ class TagArrayToStringTransformer implements DataTransformerInterface
|
|||
/**
|
||||
* Transforms a string to an array of tags.
|
||||
*
|
||||
* @param string|null $stringOfTags
|
||||
* @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
*
|
||||
* @param string|null $stringOfTags
|
||||
* @return Tag[]
|
||||
* @throws TransformationFailedException if object (issue) is not found
|
||||
* @throws TransformationFailedException
|
||||
*/
|
||||
public function reverseTransform($stringOfTags)
|
||||
{
|
||||
// check for empty tag list
|
||||
if (empty($stringOfTags)) {
|
||||
if ('' === $stringOfTags || null === $stringOfTags) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names = array_filter(array_unique(array_map('trim', explode(',', $stringOfTags))));
|
||||
|
||||
// Get the current tags and find the new ones that should be created
|
||||
// get the current tags and find the new ones that should be created
|
||||
$tags = $this->tagRepository->findBy(['name' => $names]);
|
||||
|
||||
// works, because of the implicit case: (string) $tag
|
||||
$newNames = array_diff($names, $tags);
|
||||
|
||||
foreach ($newNames as $name) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
$tags[] = $tag;
|
||||
|
||||
// There's no need to persist these new tags because Doctrine does that automatically
|
||||
// thanks to the cascade={"persist"} option in the App\Entity\Timesheet::$tags property.
|
||||
// new tags persist automatically thanks to the cascade={"persist"}
|
||||
}
|
||||
|
||||
// Return an array of tags to transform them back into a Doctrine Collection.
|
||||
// See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
return $tags;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ final class InvoiceDocumentRepository
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function addDirectory(string $directory)
|
||||
{
|
||||
$this->documentDirs[] = $directory;
|
||||
|
@ -35,6 +38,9 @@ final class InvoiceDocumentRepository
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function removeDirectory(string $directory)
|
||||
{
|
||||
if (($key = array_search($directory, $this->documentDirs)) !== false) {
|
||||
|
@ -45,7 +51,7 @@ final class InvoiceDocumentRepository
|
|||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.10 - will be removed with 2.0 - use getCustomInvoiceDirectory() instead
|
||||
* @deprecated since 1.10 - will be removed with 2.0 - use getUploadDirectory() instead
|
||||
*/
|
||||
public function getCustomInvoiceDirectory(): string
|
||||
{
|
||||
|
|
|
@ -17,10 +17,6 @@ use Twig\TwigFunction;
|
|||
|
||||
class EncoreExtension extends AbstractExtension implements ServiceSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var EntrypointLookupInterface
|
||||
*/
|
||||
private $encoreService;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
|
|
@ -113,6 +113,9 @@ class AvatarService
|
|||
return $this->directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setStorageDirectory(string $directory)
|
||||
{
|
||||
$this->directory = realpath($directory);
|
||||
|
|
|
@ -29,6 +29,9 @@ final class FileHelper
|
|||
$this->filesystem = new Filesystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setDataDirectory(string $directory)
|
||||
{
|
||||
$this->dataDir = $directory;
|
||||
|
|
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check permissions on Activities.
|
||||
*/
|
||||
class ActivityVoter extends Voter
|
||||
final class ActivityVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* support rules based on the given activity
|
||||
|
|
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check authorization on Customers.
|
||||
*/
|
||||
class CustomerVoter extends Voter
|
||||
final class CustomerVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* supported attributes/rules based on the given customer
|
||||
|
|
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check permissions on Projects.
|
||||
*/
|
||||
class ProjectVoter extends Voter
|
||||
final class ProjectVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* support rules based on the given project
|
||||
|
|
|
@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check the free-configurable permission from "kimai.permissions".
|
||||
*/
|
||||
class RolePermissionVoter extends Voter
|
||||
final class RolePermissionVoter extends Voter
|
||||
{
|
||||
private $permissionManager;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ use App\Security\RolePermissionManager;
|
|||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class TeamVoter extends Voter
|
||||
final class TeamVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* support rules based on the given $subject (here: Team)
|
||||
|
|
|
@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check permissions on Timesheets.
|
||||
*/
|
||||
class TimesheetVoter extends Voter
|
||||
final class TimesheetVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const START = 'start';
|
||||
|
|
|
@ -17,7 +17,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||
/**
|
||||
* A voter to check permissions on user profiles.
|
||||
*/
|
||||
class UserVoter extends Voter
|
||||
final class UserVoter extends Voter
|
||||
{
|
||||
private const ALLOWED_ATTRIBUTES = [
|
||||
'view',
|
||||
|
|
|
@ -237,6 +237,7 @@
|
|||
autoReloadDatatable: {% if theme_config('auto_reload_datatable') %}true{% else %}false{% endif %},
|
||||
autoComplete: {{ theme_config('autocomplete_chars') }},
|
||||
defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}',
|
||||
updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %}
|
||||
},
|
||||
{
|
||||
{% for key, translation in javascript_translations() -%}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'form': form, 'comments': comments, 'route_pin': route_pin|default(null), 'route_delete': route_delete|default(null)} %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'form': form, 'comments': comments, 'route_pin': route_pin|default(null), 'route_delete': route_delete|default(null), 'delete_by_user': delete_by_user|default(false)} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'label.comment'|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="comments_box"{% endblock %}
|
||||
{% block box_body_class %}box-comments{% endblock %}
|
||||
{% block box_body_class %}box-body-scrollable{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set replacer = {} %}
|
||||
{% for pref in app.user.preferences %}
|
||||
|
@ -10,42 +10,52 @@
|
|||
{% endfor %}
|
||||
{% if comments|length == 0 %}
|
||||
{{ 'error.no_comments_found'|trans }}
|
||||
{% endif %}
|
||||
{% for comment in comments %}
|
||||
<div class="box-comment">
|
||||
{{ widgets.user_avatar(comment.createdBy, comment.createdAt|date_full, 'img-sm') }}
|
||||
<div class="comment-text">
|
||||
<span class="username">
|
||||
{{ widgets.username(comment.createdBy) }}
|
||||
<span class="text-muted pull-right">
|
||||
{% if route_pin is not null %}
|
||||
<a href="{{ path(route_pin, {'id': comment.id}) }}" class="btn btn-default btn-xs {% if comment.pinned %}active{% endif %}"><i class="{{ 'pin'|icon }}"></i></a>
|
||||
{% elseif comment.pinned %}
|
||||
<i class="{{ 'pin'|icon }}"></i>
|
||||
{% endif %}
|
||||
{% if route_delete is not null %}
|
||||
<a href="{{ path(route_delete, {'id': comment.id}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% for comment in comments %}
|
||||
<div class="direct-chat-msg">
|
||||
<div class="direct-chat-info clearfix">
|
||||
<span class="direct-chat-name pull-left">
|
||||
{{ widgets.username(comment.createdBy) }}
|
||||
</span>
|
||||
</span>
|
||||
{{ comment.message|replace(replacer)|md2html }}
|
||||
<span class="direct-chat-timestamp pull-left">
|
||||
|
||||
{{ comment.createdAt|date_full }}
|
||||
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
{% if route_pin is not null %}
|
||||
<a href="{{ path(route_pin, {'id': comment.id}) }}" class="btn btn-default btn-xs {% if comment.pinned %}active{% endif %}"><i class="{{ 'pin'|icon }}"></i></a>
|
||||
{% elseif comment.pinned %}
|
||||
<i class="{{ 'pin'|icon }}"></i>
|
||||
{% endif %}
|
||||
{% if route_delete is not null and ((not delete_by_user) or (delete_by_user and comment.createdBy.id == app.user.id)) %}
|
||||
<a href="{{ path(route_delete, {'id': comment.id}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{{ widgets.user_avatar(comment.createdBy, false, 'direct-chat-img img-sm') }}
|
||||
<div class="direct-chat-text">
|
||||
{{ comment.message|replace(replacer)|md2html }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_footer -%}
|
||||
{% if form is not null %}
|
||||
{{ form_start(form) }}
|
||||
<div class="input-group">
|
||||
{{ widgets.user_avatar(app.user, false, 'img-responsive img-sm') }}
|
||||
<div class="img-push">
|
||||
{{ form_widget(form.message, {'attr': {'rows': '1', 'placeholder': 'placeholder.type_message'|trans}}) }}
|
||||
</div>
|
||||
<span class="input-group-btn">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ form_widget(form.message, {'attr': {'rows': '3', 'placeholder': 'placeholder.type_message'|trans, 'style': 'margin-bottom: 5px'}}) }}
|
||||
<button type="submit" class="btn btn-default">
|
||||
<i class="{{ 'comment'|icon }}"></i>
|
||||
{{ 'label.comment'|trans }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
</div>
|
||||
</div>
|
||||
{{ form_widget(form) }}
|
||||
{{ form_end(form) }}
|
||||
|
|
|
@ -232,14 +232,16 @@
|
|||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
{%- apply spaceless -%}
|
||||
{%- for icon,values in actions %}
|
||||
{% if icon == 'divider' and values is null %}
|
||||
{% if 'divider' in icon and values is null %}
|
||||
{% if not loop.last and divider is same as (false) %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
{% set divider = true %}
|
||||
{% else %}
|
||||
{% if values is iterable %}
|
||||
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
|
||||
{% if values['title'] is not defined %}
|
||||
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set values = {'url': values, 'title': icon|trans({}, 'actions')} %}
|
||||
{% endif %}
|
||||
|
@ -301,6 +303,7 @@
|
|||
{% set url = '#' %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set icon = values.icon ?? icon %}
|
||||
{% set disabled = values.disabled ?? false %}
|
||||
{% set url = values.url ?? '#' %}
|
||||
{% set onclick = values.onclick ?? null %}
|
||||
|
|
|
@ -60,12 +60,12 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% if view != 'index' and view != 'custom' %}
|
||||
{% set actions = actions|merge({'back': options.back|default(path('timesheet'))}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.timesheet', {'actions': actions, 'view': view, 'timesheet': timesheet}) %}
|
||||
{% if view == 'index' %}
|
||||
{% if view == 'index' or view == 'custom' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
|
||||
{% set actions = actions|merge({'download': {'url': path('user_export'), 'class': 'toolbar-action'}}) %}
|
||||
|
||||
{% if is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'permissions': path('admin_user_permissions')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('create_user') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_user_create')}}) %}
|
||||
{% endif %}
|
||||
|
@ -35,16 +31,12 @@
|
|||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if is_granted('view_user') %}
|
||||
{% set actions = actions|merge({'back': path('admin_user')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' and is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'permissions': path('admin_user_permissions')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'role' and is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'roles': {'url': path('admin_user_roles'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_user_roles'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'help': {'url': 'permissions.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
@ -62,15 +54,34 @@
|
|||
{% if is_granted('view', user) %}
|
||||
{% set actions = actions|merge({'profile-stats': {'url': path('user_profile', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('edit', user) %}
|
||||
{% set actions = actions|merge({'edit': path('user_profile_edit', {'username' : user.username})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('preferences', user) %}
|
||||
{% set actions = actions|merge({'settings': {'url': path('user_profile_preferences', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set subActions = {} %}
|
||||
{% if is_granted('edit', user) %}
|
||||
{% set subActions = subActions|merge({'edit': path('user_profile_edit', {'username' : user.username})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('preferences', user) %}
|
||||
{% set subActions = subActions|merge({'settings': {'url': path('user_profile_preferences', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('password', user) %}
|
||||
{% set subActions = subActions|merge({'password': {'url': path('user_profile_password', {'username' : user.username}), 'title': ('profile.password'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('api-token', user) %}
|
||||
{% set subActions = subActions|merge({'api-token': {'url': path('user_profile_api_token', {'username' : user.username}), 'title': ('profile.api-token'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('teams', user) %}
|
||||
{% set subActions = subActions|merge({'teams': {'url': path('user_profile_teams', {'username' : user.username}), 'title': ('profile.teams'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('roles', user) %}
|
||||
{% set subActions = subActions|merge({'roles': {'url': path('user_profile_roles', {'username' : user.username}), 'title': ('profile.roles'|trans)}}) %}
|
||||
{% endif %}
|
||||
{% if subActions|length > 0 %}
|
||||
{% set actions = actions|merge(subActions) %}
|
||||
{% set actions = actions|merge({'divider2': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('view_reporting') %}
|
||||
{% if view_other or app.user.id == user.id %}
|
||||
{% set actions = actions|merge({'report': path('report_user_month', {'user': user.id})}) %}
|
||||
|
@ -87,7 +98,6 @@
|
|||
{% if options.back is defined %}
|
||||
{% set actions = actions|merge({'back': options.back}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.user', {'actions': actions, 'view': view, 'user': user}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
|
|
20
templates/user/api-token.html.twig
Normal file
20
templates/user/api-token.html.twig
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends 'user/layout.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ ('profile.' ~ tab)|trans }}{% endblock %}
|
||||
{% block box_tools %}
|
||||
<a class="btn btn-box-tool" target="_blank" href="{{ path('app.swagger_ui') }}"><i class="{{ 'api'|icon('fas fa-book') }}"></i></a>
|
||||
<a class="btn btn-box-tool" target="_blank" href="{{ 'rest-api.html'|docu_link }}"><i class="{{ 'help'|icon }}"></i></a>
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
{% set tableName = 'user_admin_permissions' %}
|
||||
|
||||
{% block page_title %}{{ 'user_permissions.title'|trans }}{% endblock %}
|
||||
{% block page_title %}{{ 'profile.roles'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.user_permissions('index') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
{% extends 'user/layout.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="nav-tabs-custom">
|
||||
<ul class="nav nav-tabs">
|
||||
{% for formName, form in forms %}
|
||||
<li {% if tab == formName %}class="active"{% endif %}><a href="#{{ formName }}" data-toggle="tab" aria-expanded="false">{{ ('profile.' ~ formName)|trans }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
{% for formName, form in forms %}
|
||||
<div class="tab-pane {% if tab == formName %}active{% endif %}" id="{{ formName }}">
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ ('profile.' ~ tab)|trans }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,17 +9,17 @@
|
|||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_attributes %}id="{{ widgetId }}"{% endblock %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
{% block box_body_class %}no-padding box-body-scrollable{% endblock %}
|
||||
{% block box_title %}
|
||||
{% if not title is empty %}{{ title|trans }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-hover dataTable" role="grid">
|
||||
<tbody>
|
||||
{% for stats in projectStats %}
|
||||
{% for stats in projectStats|sort((a, b) => a.project.name <=> b.project.name) %}
|
||||
{% set project = stats.project %}
|
||||
<tr>
|
||||
<td>
|
||||
<td{% if is_granted('details', stats.project) %} class="alternative-link open-edit" data-href="{{ path('project_details', {'id': stats.project.id}) }}"{% endif %}>
|
||||
{{ widgets.label_project(stats.project) }}
|
||||
<br>
|
||||
<small>{{ widgets.label_customer(stats.project.customer) }}</small>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ title|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="{{ widgetId }}"{% endblock %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
{% block box_body_class %}no-padding box-body-scrollable{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ widgets.team_list(teams, false) }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -157,7 +157,7 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('<p>A beautiful and short comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
|
@ -173,15 +173,15 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-msg');
|
||||
self::assertStringContainsString('Blah foo bar', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body a.confirmation-link');
|
||||
self::assertEquals($this->createUrl('/admin/customer/1/comment_delete'), $node->attr('href'));
|
||||
|
||||
$this->request($client, '/admin/customer/1/comment_delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body');
|
||||
self::assertStringContainsString('There were no comments posted yet', $node->html());
|
||||
}
|
||||
|
||||
|
@ -197,15 +197,15 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('Blah foo bar', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text a.btn.active');
|
||||
self::assertEquals(0, $node->count());
|
||||
|
||||
$this->request($client, '/admin/customer/1/comment_pin');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active');
|
||||
self::assertEquals(1, $node->count());
|
||||
self::assertEquals($this->createUrl('/admin/customer/1/comment_pin'), $node->attr('href'));
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ class PermissionControllerTest extends ControllerBaseTest
|
|||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 119);
|
||||
$this->assertPageActions($client, [
|
||||
'back' => $this->createUrl('/admin/user/'),
|
||||
'roles modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
//'back' => $this->createUrl('/admin/user/'),
|
||||
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
'help' => 'https://www.kimai.org/documentation/permissions.html'
|
||||
]);
|
||||
|
||||
|
|
|
@ -93,32 +93,20 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||
|
||||
public function getTabTestData()
|
||||
{
|
||||
$userTabs = ['#settings', '#password', '#api-token'];
|
||||
|
||||
return [
|
||||
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#settings', '#password', '#api-token']],
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, array_merge($userTabs, ['#teams', '#roles'])],
|
||||
[User::ROLE_USER, UserFixtures::USERNAME_USER],
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTabTestData
|
||||
*/
|
||||
public function testEditActionTabs($role, $username, $expectedTabs)
|
||||
public function testEditActionTabs($role, $username)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, '/profile/' . $username . '/edit');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||
$this->assertEquals(\count($expectedTabs), $tabs->count());
|
||||
$foundTabs = [];
|
||||
|
||||
/** @var \DOMElement $tab */
|
||||
foreach ($tabs->filter('a') as $tab) {
|
||||
$foundTabs[] = $tab->getAttribute('href');
|
||||
}
|
||||
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
||||
}
|
||||
|
||||
public function testIndexActionWithDifferentUsername()
|
||||
|
|
|
@ -234,7 +234,7 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('<p>A beautiful and long comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
|
@ -250,15 +250,15 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('Foo bar blub', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body a.confirmation-link');
|
||||
self::assertEquals($this->createUrl('/admin/project/1/comment_delete'), $node->attr('href'));
|
||||
|
||||
$this->request($client, '/admin/project/1/comment_delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body');
|
||||
self::assertStringContainsString('There were no comments posted yet', $node->html());
|
||||
}
|
||||
|
||||
|
@ -274,15 +274,15 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
|
||||
self::assertStringContainsString('Foo bar blub', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active');
|
||||
self::assertEquals(0, $node->count());
|
||||
|
||||
$this->request($client, '/admin/project/1/comment_pin');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-body a.btn.active');
|
||||
self::assertEquals(1, $node->count());
|
||||
self::assertEquals($this->createUrl('/admin/project/1/comment_pin'), $node->attr('href'));
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ class UserControllerTest extends ControllerBaseTest
|
|||
'search search-toggle visible-xs-inline' => '#',
|
||||
'visibility' => '#',
|
||||
'download toolbar-action' => $this->createUrl('/admin/user/export'),
|
||||
'permissions' => $this->createUrl('/admin/permissions'),
|
||||
'create' => $this->createUrl('/admin/user/create'),
|
||||
'help' => 'https://www.kimai.org/documentation/users.html'
|
||||
]);
|
||||
|
@ -117,17 +116,6 @@ class UserControllerTest extends ControllerBaseTest
|
|||
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
||||
$client->followRedirect();
|
||||
|
||||
$expectedTabs = ['#settings', '#password', '#api-token', '#teams', '#roles'];
|
||||
|
||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||
$this->assertEquals(\count($expectedTabs), $tabs->count());
|
||||
$foundTabs = [];
|
||||
/** @var \DOMElement $tab */
|
||||
foreach ($tabs->filter('a') as $tab) {
|
||||
$foundTabs[] = $tab->getAttribute('href');
|
||||
}
|
||||
$this->assertEmpty(array_diff($expectedTabs, $foundTabs));
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=user_edit]')->form();
|
||||
$this->assertEquals($username, $form->get('user_edit[alias]')->getValue());
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ class TagTest extends TestCase
|
|||
$this->assertEquals('foo', $sut->getName());
|
||||
$this->assertEquals('foo', (string) $sut);
|
||||
|
||||
$sut->setName(null);
|
||||
$this->assertNull($sut->getName());
|
||||
|
||||
$this->assertInstanceOf(Tag::class, $sut->setColor('#fffccc'));
|
||||
$this->assertEquals('#fffccc', $sut->getColor());
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ class UserTest extends TestCase
|
|||
public function testTeams()
|
||||
{
|
||||
$sut = new User();
|
||||
$user = new User();
|
||||
$team = new Team();
|
||||
self::assertEmpty($sut->getTeams());
|
||||
self::assertEmpty($team->getUsers());
|
||||
|
@ -151,6 +152,10 @@ class UserTest extends TestCase
|
|||
self::assertSame($team, $sut->getTeams()[0]);
|
||||
self::assertSame($sut, $team->getUsers()[0]);
|
||||
self::assertTrue($sut->hasTeamAssignment());
|
||||
self::assertFalse($sut->hasTeamMember($user));
|
||||
|
||||
$team->addUser($user);
|
||||
self::assertTrue($sut->hasTeamMember($user));
|
||||
|
||||
self::assertFalse($sut->isTeamleadOf($team));
|
||||
self::assertTrue($sut->isInTeam($team));
|
||||
|
|
35
tests/Event/TimesheetDuplicatePostEventTest.php
Normal file
35
tests/Event/TimesheetDuplicatePostEventTest.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Event\AbstractTimesheetEvent;
|
||||
use App\Event\TimesheetDuplicatePostEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\TimesheetDuplicatePostEvent
|
||||
*/
|
||||
class TimesheetDuplicatePostEventTest extends AbstractTimesheetEventTest
|
||||
{
|
||||
protected function createTimesheetEvent(Timesheet $timesheet): AbstractTimesheetEvent
|
||||
{
|
||||
return new TimesheetDuplicatePostEvent($timesheet, new Timesheet());
|
||||
}
|
||||
|
||||
public function testGetOriginalTimesheet()
|
||||
{
|
||||
$newTimesheet = new Timesheet();
|
||||
$originalTimesheet = new Timesheet();
|
||||
$sut = new TimesheetDuplicatePostEvent($newTimesheet, $originalTimesheet);
|
||||
|
||||
self::assertSame($newTimesheet, $sut->getTimesheet());
|
||||
self::assertSame($originalTimesheet, $sut->getOriginalTimesheet());
|
||||
}
|
||||
}
|
35
tests/Event/TimesheetDuplicatePreEventTest.php
Normal file
35
tests/Event/TimesheetDuplicatePreEventTest.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?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\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Event\AbstractTimesheetEvent;
|
||||
use App\Event\TimesheetDuplicatePreEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\TimesheetDuplicatePreEvent
|
||||
*/
|
||||
class TimesheetDuplicatePreEventTest extends AbstractTimesheetEventTest
|
||||
{
|
||||
protected function createTimesheetEvent(Timesheet $timesheet): AbstractTimesheetEvent
|
||||
{
|
||||
return new TimesheetDuplicatePreEvent($timesheet, new Timesheet());
|
||||
}
|
||||
|
||||
public function testGetOriginalTimesheet()
|
||||
{
|
||||
$newTimesheet = new Timesheet();
|
||||
$originalTimesheet = new Timesheet();
|
||||
$sut = new TimesheetDuplicatePreEvent($newTimesheet, $originalTimesheet);
|
||||
|
||||
self::assertSame($newTimesheet, $sut->getTimesheet());
|
||||
self::assertSame($originalTimesheet, $sut->getOriginalTimesheet());
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ class UserPreferenceSubscriberTest extends TestCase
|
|||
self::assertSame($user, $event->getUser());
|
||||
|
||||
$prefs = $sut->getDefaultPreferences($user);
|
||||
self::assertCount(12, $prefs);
|
||||
self::assertCount(13, $prefs);
|
||||
|
||||
foreach ($prefs as $pref) {
|
||||
switch ($pref->getName()) {
|
||||
|
@ -70,7 +70,7 @@ class UserPreferenceSubscriberTest extends TestCase
|
|||
// TODO test merging values
|
||||
$sut->loadUserPreferences($event);
|
||||
$prefs = $event->getUser()->getPreferences();
|
||||
self::assertCount(12, $prefs);
|
||||
self::assertCount(13, $prefs);
|
||||
|
||||
foreach ($prefs as $pref) {
|
||||
switch ($pref->getName()) {
|
||||
|
|
30
tests/Utils/AvatarServiceTest.php
Normal file
30
tests/Utils/AvatarServiceTest.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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\Utils;
|
||||
|
||||
use App\Utils\AvatarService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Utils\AvatarService
|
||||
*/
|
||||
class AvatarServiceTest extends TestCase
|
||||
{
|
||||
public function testDataDirectory()
|
||||
{
|
||||
$data = realpath(__DIR__ . '/../../');
|
||||
$sut = new AvatarService($data);
|
||||
self::assertEquals($data . '/public/avatars', $sut->getStorageDirectory());
|
||||
|
||||
$data = realpath(__DIR__ . '/../../var/data/');
|
||||
$sut->setStorageDirectory($data);
|
||||
self::assertEquals($data, $sut->getStorageDirectory());
|
||||
}
|
||||
}
|
|
@ -39,4 +39,19 @@ class FileHelperTest extends TestCase
|
|||
{
|
||||
self::assertEquals($expected, FileHelper::convertToAsciiFilename($original));
|
||||
}
|
||||
|
||||
public function testDataDirectory()
|
||||
{
|
||||
$data = realpath(__DIR__ . '/../_data/');
|
||||
$sut = new FileHelper($data);
|
||||
self::assertEquals($data . '/', $sut->getDataDirectory());
|
||||
self::assertEquals($data . '/foo/', $sut->getDataDirectory('/foo/'));
|
||||
self::assertEquals($data . '/foo/', $sut->getDataDirectory('foo'));
|
||||
|
||||
$data = realpath(__DIR__ . '/../../var/data/');
|
||||
$sut->setDataDirectory($data);
|
||||
self::assertEquals($data . '/', $sut->getDataDirectory());
|
||||
self::assertEquals($data . '/foo/', $sut->getDataDirectory('/foo/'));
|
||||
self::assertEquals($data . '/foo/', $sut->getDataDirectory('foo'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -552,6 +552,10 @@
|
|||
<source>label.timesheet.export_decimal</source>
|
||||
<target>Dezimal Format für Export nutzen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="theme.update_browser_title">
|
||||
<source>theme.update_browser_title</source>
|
||||
<target>Browser Titel aktualisieren</target>
|
||||
</trans-unit>
|
||||
<!--
|
||||
User timesheet calendar
|
||||
-->
|
||||
|
|
|
@ -560,7 +560,10 @@
|
|||
<source>label.timesheet.export_decimal</source>
|
||||
<target>Use decimal duration in export</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="theme.update_browser_title">
|
||||
<source>theme.update_browser_title</source>
|
||||
<target>Update browser title</target>
|
||||
</trans-unit>
|
||||
<!--
|
||||
User timesheet calendar
|
||||
-->
|
||||
|
|
|
@ -1881,9 +1881,9 @@ caniuse-api@^3.0.0:
|
|||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001093:
|
||||
version "1.0.30001111"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001111.tgz#dd0ce822c70eb6c7c068e4a55c22e19ec1501298"
|
||||
integrity sha512-xnDje2wchd/8mlJu8sXvWxOGvMgv+uT3iZ3bkIAynKOzToCssWCmkz/ZIkQBs/2pUB4uwnJKVORWQ31UkbVjOg==
|
||||
version "1.0.30001180"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001180.tgz"
|
||||
integrity sha512-n8JVqXuZMVSPKiPiypjFtDTXc4jWIdjxull0f92WLo7e1MSi3uJ3NvveakSh/aCl1QKFAvIz3vIj0v+0K+FrXw==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
|
|
Loading…
Reference in a new issue