Release 2.4.0 (#4427)

* button to duplicate old timesheets, even those from lockdown period
* bump composer packages
* fixes tooltip remains in view #4426
* fix js error if all widgets were removed
* allow to open timesheet edit dialog from export listing
* css classes for timesheet context menu, so they can be hidden
* added flag to force a user to set password upon login
* enable lazy-ghost-objects to fix deprecation
* change user, username, internal_rate export column labels
* helper method to find user by displayname
* log improvements and format changes
* deactivate broken schema validation
This commit is contained in:
Kevin Papst
2023-11-19 16:05:43 +01:00
committed by GitHub
parent 9d8e5ecd7a
commit 20164295f8
27 changed files with 186 additions and 169 deletions

View File

@@ -207,6 +207,7 @@ final class UserController extends BaseApiController
'include_active_flag' => ($profile->getId() !== $this->getUser()->getId()),
'include_preferences' => $this->isGranted('preferences', $profile),
'include_supervisor' => $this->isGranted('supervisor', $profile),
'include_password_reset' => $this->isGranted('password', $profile),
]);
$form->setData($profile);

View File

@@ -399,7 +399,7 @@ final class SystemConfiguration
public function getTimesheetTrackingMode(): string
{
return (string) $this->find('timesheet.mode');
return $this->getString('timesheet.mode', 'default');
}
public function isTimesheetMarkdownEnabled(): bool
@@ -442,19 +442,6 @@ final class SystemConfiguration
return (int) $this->find('timesheet.rounding.default.duration');
}
private function getIncrement(string $key, int $fallback, int $min = 1): int
{
$config = $this->find($key);
if ($config === null || trim($config) === '') {
return $fallback;
}
$config = (int) $config;
return max($config, $min);
}
public function getTimesheetIncrementDuration(): int
{
return $this->getIncrement('timesheet.duration_increment', $this->getTimesheetDefaultRoundingDuration(), 0);
@@ -511,4 +498,36 @@ final class SystemConfiguration
{
return $this->find('project.copy_teams_on_create') === true;
}
// ========== Helper functions ==========
private function getIncrement(string $key, int $fallback, int $min = 1): int
{
$config = $this->find($key);
if ($config === null || trim($config) === '') {
return $fallback;
}
$config = (int) $config;
return max($config, $min);
}
private function getString(string $key, string $fallback): string
{
$config = $this->find($key);
if ($config === null) {
return $fallback;
}
$config = (string) $config;
if (trim($config) === '') {
return $fallback;
}
return $config;
}
}

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.3.0';
public const VERSION = '2.4.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20300;
public const VERSION_ID = 20400;
/**
* The software name
*/

View File

@@ -318,6 +318,7 @@ final class ProfileController extends AbstractController
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
'include_preferences' => true,
'include_supervisor' => $this->isGranted('supervisor', $user),
'include_password_reset' => $this->isGranted('password', $user),
]
);
}

View File

@@ -13,25 +13,20 @@ use Doctrine\Common\Collections\Collection;
interface ExportableItem
{
public function getId(): ?int;
/**
* Whether this item was already exported.
*
* @return bool
*/
public function isExported(): bool;
/**
* Whether this item should be included in invoices.
*
* @return bool
*/
public function isBillable(): bool;
/**
* Returns the named meta field or null.
*
* @param string $name
* @return MetaTableTypeInterface|null
*/
public function getMetaField(string $name): ?MetaTableTypeInterface;
@@ -44,8 +39,6 @@ interface ExportableItem
/**
* Returns the amount for this item.
*
* @return float
*/
public function getAmount(): float;
@@ -83,15 +76,13 @@ interface ExportableItem
/**
* A name representation for this type of item.
*
* @return string
* Example: "timesheet"
*/
public function getType(): string;
/**
* A name representation for the category of this item.
*
* @return string
* Example: "work"
*/
public function getCategory(): string;
}

View File

@@ -52,6 +52,8 @@ class Tag
use ColorTrait;
/**
* This is ONLY here, so we can count the amount of timesheets.
*
* @var Collection<Timesheet>
*/
#[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
@@ -89,26 +91,6 @@ class Tag
$this->visible = $visible;
}
public function addTimesheet(Timesheet $timesheet): void
{
if ($this->timesheets->contains($timesheet)) {
return;
}
$this->timesheets->add($timesheet);
$timesheet->addTag($this);
}
public function removeTimesheet(Timesheet $timesheet): void
{
if (!$this->timesheets->contains($timesheet)) {
return;
}
$this->timesheets->removeElement($timesheet);
$timesheet->removeTag($this);
}
public function __toString(): string
{
return $this->getName();

View File

@@ -411,10 +411,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
return $this->internalRate;
}
/**
* @param Tag $tag
* @return Timesheet
*/
public function addTag(Tag $tag): Timesheet
{
if ($this->tags->contains($tag)) {
@@ -425,9 +421,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
return $this;
}
/**
* @param Tag $tag
*/
public function removeTag(Tag $tag): void
{
if (!$this->tags->contains($tag)) {

View File

@@ -151,16 +151,13 @@ class PageActionsEvent extends ThemeEvent
$this->addAction('create', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'title' => 'create', 'accesskey' => 'a']);
}
public function addEdit(string $url, bool $modal = true): void
public function addEdit(string $url, bool $modal = true, string $class = ''): void
{
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'translation_domain' => 'actions', 'title' => 'edit']);
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' . ($class === '' ? '' : ' ' . $class) : $class), 'translation_domain' => 'actions', 'title' => 'edit']);
}
/**
* Link to a configuration section.
*
* @param string $url
* @return void
*/
public function addSettings(string $url): void
{

View File

@@ -25,20 +25,20 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
$timesheet = $payload['timesheet'];
if ($timesheet->getId() !== null) {
if ($timesheet->isRunning() && $this->isGranted('stop', $timesheet)) {
$event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
$event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-stop', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
}
if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) {
$event->addAction('repeat', ['title' => 'repeat', 'translation_domain' => 'actions', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
$event->addAction('repeat', ['title' => 'repeat', 'translation_domain' => 'actions', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
}
if ($this->isGranted('edit', $timesheet)) {
$event->addEdit($this->path($routeEdit, ['id' => $timesheet->getId()]), !$event->isView('edit'));
$event->addEdit($this->path($routeEdit, ['id' => $timesheet->getId()]), !$event->isView('edit'), 'dd-ts-edit');
}
if ($this->isGranted('duplicate', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('copy', ['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);
$event->addAction('copy', ['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class . ' dd-ts-duplicate']);
}
if ($event->countActions() > 0) {
@@ -48,7 +48,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
if (($event->isIndexView() || $event->isView('calendar')) && $this->isGranted('delete', $timesheet)) {
$event->addAction('trash', [
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
'class' => 'api-link text-red',
'class' => 'api-link text-red dd-ts-trash',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.timesheetDelete',

View File

@@ -73,8 +73,12 @@ abstract class AbstractSpreadsheetRenderer
'end' => [],
'duration' => [],
'rate' => [],
'rate_internal' => [],
'user' => [],
'rate_internal' => [
'label' => 'internalRate', // different translation key
],
'user' => [
'label' => 'name'
],
'username' => [],
'customer' => [],
'project' => [],
@@ -309,13 +313,6 @@ abstract class AbstractSpreadsheetRenderer
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $username);
};
}
if (!isset($columns['username']['header'])) {
$columns['username']['header'] = function (Worksheet $sheet, int $row, int $column): int {
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('name'));
return 1;
};
}
}
if (isset($columns['customer']) && !isset($columns['customer']['render'])) {
@@ -675,7 +672,7 @@ abstract class AbstractSpreadsheetRenderer
$amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn);
$recordsHeaderColumn += $amount;
} else {
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($label));
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans((\array_key_exists('label', $settings) && \is_string($settings['label'])) ? $settings['label'] : $label));
}
}

View File

@@ -28,7 +28,7 @@ class SpreadsheetExporter
/**
* @var CellFormatterInterface[]
*/
private $formatter = [];
private array $formatter = [];
public function __construct(private TranslatorInterface $translator)
{

View File

@@ -96,6 +96,14 @@ class UserEditType extends AbstractType
'ignore_users' => ($user instanceof User && $user->getId() !== null ? [$user] : []),
]);
}
if ($options['include_password_reset']) {
$builder->add('requiresPasswordReset', YesNoType::class, [
'label' => 'force_password_change',
'help' => 'force_password_change_help',
'required' => false,
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
@@ -109,6 +117,7 @@ class UserEditType extends AbstractType
'include_active_flag' => true,
'include_preferences' => true,
'include_supervisor' => true,
'include_password_reset' => true,
]);
}
}

View File

@@ -0,0 +1,28 @@
<?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\Logger;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\LogRecord;
final class LogProcessor
{
#[AsMonologProcessor]
public function __invoke(LogRecord $record): LogRecord
{
if (\array_key_exists('bundle', $record->context)) {
$record->extra['channel'] = strtoupper($record->context['bundle']);
} else {
$record->extra['channel'] = $record->channel;
}
return $record;
}
}

View File

@@ -100,16 +100,16 @@ class TagRepository extends EntityRepository
public function getTagCount(TagQuery $query): Pagination
{
$qb = $this->getQueryBuilderForQuery($query);
$qb1 = clone $qb;
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->count('tag.name'))
->select($qb->expr()->count('tag.id'))
;
$counter = (int) $qb->getQuery()->getSingleScalarResult();
$qb = $this->getQueryBuilderForQuery($query);
$paginator = new QueryBuilderPaginator($qb, $counter);
$paginator = new QueryBuilderPaginator($qb1, $counter);
$pager = new Pagination($paginator);
$pager->setMaxPerPage($query->getPageSize());

View File

@@ -142,6 +142,11 @@ class UserService
return $this->repository->findByUsername($name);
}
public function findUserByDisplayName(string $name): ?User
{
return $this->repository->findOneBy(['alias' => $name]);
}
public function findUserByConfirmationToken(string $token): ?User
{
return $this->repository->findOneBy(['confirmationToken' => $token]);

View File

@@ -105,7 +105,7 @@ final class TimesheetVoter extends Voter
break;
case 'duplicate':
if (!$this->canDuplicate($user, $subject)) {
if (!$this->canStart($subject)) {
return false;
}
$permission = self::EDIT;
@@ -194,15 +194,6 @@ final class TimesheetVoter extends Voter
return true;
}
private function canDuplicate(User $user, Timesheet $timesheet): bool
{
if (!$this->isAllowedInLockdown($user, $timesheet)) {
return false;
}
return true;
}
private function isAllowedExported(User $user, Timesheet $timesheet): bool
{
if (!$timesheet->isExported()) {