Skip to content

Commit

Permalink
Release 2.7.0 (#4506)
Browse files Browse the repository at this point in the history
* added api URL for simpler integration
* allow to request password change upon next login
* make ModifiedAt timesheet independent
* improved plugin api
* replace kernel calls with AutoconfigureTag and TaggedIterator attributes
* new translation keys (e.g. for days)
* support setting min and max date on date and daterange pickers
  • Loading branch information
kevinpapst committed Dec 26, 2023
1 parent f86eca0 commit ad645f5
Show file tree
Hide file tree
Showing 72 changed files with 379 additions and 736 deletions.
12 changes: 11 additions & 1 deletion assets/js/forms/KimaiDatePicker.js
Expand Up @@ -57,6 +57,16 @@ export default class KimaiDatePicker extends KimaiFormPlugin {
if (element.dataset.format === undefined) {
console.log('Trying to bind litepicker to an element without data-format attribute');
}
if (element.hasAttribute('min') !== undefined) {
options = {...options, ...{
'minDate': element.getAttribute('min'),
}};
}
if (element.hasAttribute('max') !== undefined) {
options = {...options, ...{
'maxDate': element.getAttribute('max'),
}};
}
options = {...options, ...{
format: element.dataset.format,
showTooltip: false,
Expand All @@ -66,7 +76,7 @@ export default class KimaiDatePicker extends KimaiFormPlugin {
firstDay: FIRST_DOW, // Litepicker: 0 = Sunday, 1 = Monday
setup: (picker) => {
// nasty hack, because litepicker does not trigger change event on the input and the available
// event "selected" is triggered why to often, even when moving the cursor inside the input
// event "selected" is triggered way to often, even when moving the cursor inside the input
// element (not even typing is necessary) and so we have to make sure that the manual "click" event
// (works for touch as well) happened before we actually dispatch the change event manually ...
// what? report forms would be submitted upon cursor move without the "preselect” check
Expand Down
32 changes: 2 additions & 30 deletions config/services.yaml
Expand Up @@ -60,18 +60,6 @@ services:
arguments:
$cacheDirectory: '%kernel.cache_dir%'

App\Plugin\PluginManager:
arguments: [!tagged kimai.plugin]

App\Validator\Constraints\TimesheetValidator:
arguments: [!tagged timesheet.validator]

App\Validator\Constraints\ProjectValidator:
arguments: [!tagged project.validator]

App\Validator\Constraints\QuickEntryTimesheetValidator:
arguments: [!tagged timesheet.validator]

App\Utils\FileHelper:
arguments:
$dataDir: '%kimai.data_dir%'
Expand All @@ -83,35 +71,19 @@ services:
arguments:
$mailer: '@App\Mail\KimaiMailer'

# ================================================================================
# DATABASE
# ================================================================================

# updates timesheet records and apply configured rate & rounding rules
App\Doctrine\TimesheetSubscriber:
class: App\Doctrine\TimesheetSubscriber
arguments: [!tagged timesheet.calculator]

# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
App\Doctrine\ModifiedSubscriber:
class: App\Doctrine\ModifiedSubscriber

# ================================================================================
# TIMESHEET RECORD CALCULATOR
# ================================================================================

App\Timesheet\RoundingService:
arguments:
$roundingModes: !tagged timesheet.rounding_mode
# this is currently required, as local.yaml allows to configure several rules,
# while the database system only allows one rounding rule
$rules: '%kimai.timesheet.rounding%'

App\Timesheet\RateService:
arguments: ['%kimai.timesheet.rates%']

App\Timesheet\TrackingModeService:
arguments:
$modes: !tagged timesheet.tracking_mode

# ================================================================================
# SECURITY & VOTER
# ================================================================================
Expand Down
35 changes: 0 additions & 35 deletions phpstan.neon
Expand Up @@ -81,11 +81,6 @@ parameters:
count: 1
path: src/API/Model/PageAction.php

-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/API/Model/Plugin.php

-
message: "#^Property App\\\\API\\\\Model\\\\Plugin\\:\\:\\$name is never read, only written\\.$#"
count: 1
Expand Down Expand Up @@ -526,16 +521,6 @@ parameters:
count: 1
path: src/Command/InvoiceCreateCommand.php

-
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/Command/PluginCommand.php

-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/Command/PluginCommand.php

-
message: "#^Method App\\\\Command\\\\PromoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#"
count: 1
Expand Down Expand Up @@ -1936,11 +1921,6 @@ parameters:
count: 2
path: src/EventSubscriber/Actions/ActivitySubscriber.php

-
message: "#^Cannot call method getHomepage\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/EventSubscriber/Actions/PluginSubscriber.php

-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
Expand Down Expand Up @@ -5231,21 +5211,6 @@ parameters:
count: 1
path: src/Timesheet/Rounding/FloorRounding.php

-
message: "#^Method App\\\\Timesheet\\\\RoundingService\\:\\:__construct\\(\\) has parameter \\$rules with no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php

-
message: "#^Method App\\\\Timesheet\\\\RoundingService\\:\\:getRoundingRules\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php

-
message: "#^Property App\\\\Timesheet\\\\RoundingService\\:\\:\\$rulesCache type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php

-
message: "#^Cannot call method getTimezone\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 2
Expand Down
2 changes: 0 additions & 2 deletions public/build/app.63b27bee.js

This file was deleted.

2 changes: 2 additions & 0 deletions public/build/app.adf0b03a.js

Large diffs are not rendered by default.

File renamed without changes.
4 changes: 2 additions & 2 deletions public/build/entrypoints.json
Expand Up @@ -3,7 +3,7 @@
"app": {
"js": [
"/build/runtime.f0079159.js",
"/build/app.63b27bee.js"
"/build/app.adf0b03a.js"
],
"css": [
"/build/app.0ecb28c1.css"
Expand Down Expand Up @@ -63,7 +63,7 @@
},
"integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.63b27bee.js": "sha384-1CV+he7m+KfetJpq/DmRwKfUhKH4L7MImv7rRUz/mr/KNR3VkT0fQJHF7ViPTVYE",
"/build/app.adf0b03a.js": "sha384-z0Evh/1m2s8eBABcPDKISlFOKIzUpZE5CJt7HQIVgciHgD8GxpK50XZyNCNq+gyd",
"/build/app.0ecb28c1.css": "sha384-iCao48T4VAR0rv39D1+kCk8GCRtve2QDTH5gGq1I1TMXmy1xA/OPxjeBFgGKkRk+",
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
Expand Down
2 changes: 1 addition & 1 deletion public/build/manifest.json
@@ -1,6 +1,6 @@
{
"build/app.css": "/build/app.0ecb28c1.css",
"build/app.js": "/build/app.63b27bee.js",
"build/app.js": "/build/app.adf0b03a.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
"build/invoice.css": "/build/invoice.5bea118e.css",
Expand Down
1 change: 0 additions & 1 deletion src/API/StatusController.php
Expand Up @@ -66,7 +66,6 @@ public function pluginAction(PluginManager $pluginManager): Response
{
$plugins = [];
foreach ($pluginManager->getPlugins() as $plugin) {
$pluginManager->loadMetadata($plugin);
$plugins[] = new Plugin($plugin);
}

Expand Down
6 changes: 6 additions & 0 deletions src/Command/CreateUserCommand.php
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

Expand Down Expand Up @@ -43,6 +44,7 @@ protected function configure(): void
User::DEFAULT_ROLE
)
->addArgument('password', InputArgument::OPTIONAL, 'Password for the new user (requested if not provided)')
->addOption('request-password', null, InputOption::VALUE_NONE, 'The user needs to set a new password during next login')
;
}

Expand All @@ -69,6 +71,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$user->setEnabled(true);
$user->setRoles(explode(',', $role));

if ($input->getOption('request-password') === true) {
$user->setRequiresPasswordReset(true);
}

try {
$this->userService->saveNewUser($user);
$io->success(sprintf('Success! Created user: %s', $username));
Expand Down
4 changes: 2 additions & 2 deletions src/Command/PluginCommand.php
Expand Up @@ -48,16 +48,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$rows = [];
foreach ($plugins as $plugin) {
$this->plugins->loadMetadata($plugin);
$meta = $plugin->getMetadata();
$rows[] = [
$plugin->getId(),
$plugin->getName(),
$meta->getVersion(),
$meta->getKimaiVersion(),
$plugin->getPath(),
];
}
$io->table(['Name', 'Version', 'Requires', 'Directory'], $rows);
$io->table(['Id', 'Name', 'Version', 'Requires', 'Directory'], $rows);

return Command::SUCCESS;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Constants.php
Expand Up @@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.6.0';
public const VERSION = '2.7.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20600;
public const VERSION_ID = 20700;
/**
* The software name
*/
Expand Down
1 change: 0 additions & 1 deletion src/Controller/PluginController.php
Expand Up @@ -28,7 +28,6 @@ public function indexAction(PluginManager $manager, HttpClientInterface $client,
$installed = [];
$plugins = $manager->getPlugins();
foreach ($plugins as $plugin) {
$manager->loadMetadata($plugin);
$installed[] = $plugin->getId();
}

Expand Down
10 changes: 6 additions & 4 deletions src/DependencyInjection/Compiler/ExportServiceCompilerPass.php
Expand Up @@ -9,8 +9,10 @@

namespace App\DependencyInjection\Compiler;

use App\Export\ExportRepositoryInterface;
use App\Export\RendererInterface;
use App\Export\ServiceExport;
use App\Kernel;
use App\Export\TimesheetExportInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -24,17 +26,17 @@ public function process(ContainerBuilder $container): void
{
$definition = $container->findDefinition(ServiceExport::class);

$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
$taggedRenderer = $container->findTaggedServiceIds(RendererInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('addRenderer', [new Reference($id)]);
}

$taggedExporter = $container->findTaggedServiceIds(Kernel::TAG_TIMESHEET_EXPORTER);
$taggedExporter = $container->findTaggedServiceIds(TimesheetExportInterface::class);
foreach ($taggedExporter as $id => $tags) {
$definition->addMethodCall('addTimesheetExporter', [new Reference($id)]);
}

$taggedRepository = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_REPOSITORY);
$taggedRepository = $container->findTaggedServiceIds(ExportRepositoryInterface::class);
foreach ($taggedRepository as $id => $tags) {
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
}
Expand Down
13 changes: 8 additions & 5 deletions src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php
Expand Up @@ -9,8 +9,11 @@

namespace App\DependencyInjection\Compiler;

use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\RendererInterface;
use App\Invoice\ServiceInvoice;
use App\Kernel;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -24,22 +27,22 @@ public function process(ContainerBuilder $container): void
{
$definition = $container->findDefinition(ServiceInvoice::class);

$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_RENDERER);
$taggedRenderer = $container->findTaggedServiceIds(RendererInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('addRenderer', [new Reference($id)]);
}

$taggedGenerator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_NUMBER_GENERATOR);
$taggedGenerator = $container->findTaggedServiceIds(NumberGeneratorInterface::class);
foreach ($taggedGenerator as $id => $tags) {
$definition->addMethodCall('addNumberGenerator', [new Reference($id)]);
}

$taggedCalculator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_CALCULATOR);
$taggedCalculator = $container->findTaggedServiceIds(CalculatorInterface::class);
foreach ($taggedCalculator as $id => $tags) {
$definition->addMethodCall('addCalculator', [new Reference($id)]);
}

$taggedRepository = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_REPOSITORY);
$taggedRepository = $container->findTaggedServiceIds(InvoiceItemRepositoryInterface::class);
foreach ($taggedRepository as $id => $tags) {
$definition->addMethodCall('addInvoiceItemRepository', [new Reference($id)]);
}
Expand Down
4 changes: 2 additions & 2 deletions src/DependencyInjection/Compiler/WidgetCompilerPass.php
Expand Up @@ -9,7 +9,7 @@

namespace App\DependencyInjection\Compiler;

use App\Kernel;
use App\Widget\WidgetInterface;
use App\Widget\WidgetService;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -24,7 +24,7 @@ public function process(ContainerBuilder $container): void
{
$definition = $container->findDefinition(WidgetService::class);

$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_WIDGET);
$taggedRenderer = $container->findTaggedServiceIds(WidgetInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('registerWidget', [new Reference($id)]);
}
Expand Down
15 changes: 15 additions & 0 deletions src/Doctrine/ModifiedAt.php
@@ -0,0 +1,15 @@
<?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\Doctrine;

interface ModifiedAt
{
public function setModifiedAt(\DateTimeImmutable $dateTime): void;
}
13 changes: 5 additions & 8 deletions src/Doctrine/ModifiedSubscriber.php
Expand Up @@ -9,14 +9,13 @@

namespace App\Doctrine;

use App\Entity\Timesheet;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;

/**
* Automatically set the modifiedAt field for all Timesheet entries.
* Automatically set the modifiedAt field for all ModifiedAt instances.
*/
#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
Expand All @@ -34,17 +33,15 @@ public function onFlush(OnFlushEventArgs $args): void
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));

foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!($entity instanceof Timesheet)) {
continue;
if ($entity instanceof ModifiedAt) {
$entity->setModifiedAt($now);
}
$entity->setModifiedAt($now);
}

foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!($entity instanceof Timesheet)) {
continue;
if ($entity instanceof ModifiedAt) {
$entity->setModifiedAt($now);
}
$entity->setModifiedAt($now);
}
}
}

0 comments on commit ad645f5

Please sign in to comment.