Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -10,23 +10,17 @@
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Constants;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
final class Configuration extends AbstractExtension
|
||||
{
|
||||
private $configuration;
|
||||
private $cache = [];
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('config', [$this, 'get']),
|
||||
@@ -35,30 +29,31 @@ final class Configuration extends AbstractExtension
|
||||
|
||||
public function get(string $name)
|
||||
{
|
||||
if (\array_key_exists($name, $this->cache)) {
|
||||
return $this->cache[$name];
|
||||
switch ($name) {
|
||||
case 'chart-class':
|
||||
return ''; // 'chart';
|
||||
case 'theme.chart.background_color':
|
||||
return '#3c8dbc';
|
||||
case 'theme.chart.border_color':
|
||||
return '#3b8bba';
|
||||
case 'theme.chart.grid_color':
|
||||
return 'rgba(0,0,0,.05)';
|
||||
case 'theme.chart.height':
|
||||
return '300';
|
||||
case 'theme.calendar.background_color':
|
||||
return Constants::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
$value = $this->configuration->find($name);
|
||||
$this->cache[$name] = $value;
|
||||
|
||||
return $value;
|
||||
return $this->configuration->find($name);
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
if (\array_key_exists($name, $this->cache)) {
|
||||
return $this->cache[$name];
|
||||
}
|
||||
|
||||
$checks = ['is' . $name, 'get' . $name, 'has' . $name, $name];
|
||||
|
||||
foreach ($checks as $methodName) {
|
||||
if (method_exists($this->configuration, $methodName)) {
|
||||
$value = \call_user_func([$this->configuration, $methodName], $arguments);
|
||||
$this->cache[$name] = $value;
|
||||
|
||||
return $value;
|
||||
return \call_user_func([$this->configuration, $methodName], $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
65
src/Twig/Context.php
Normal file
65
src/Twig/Context.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class Context
|
||||
{
|
||||
public function __construct(private SystemConfiguration $systemConfiguration, private RequestStack $requestStack)
|
||||
{
|
||||
}
|
||||
|
||||
public function isModalRequest(): bool
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
if ($request === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->isXmlHttpRequest()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$request->headers->has('X-Requested-With')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains(strtolower($request->headers->get('X-Requested-With')), 'kimai-modal');
|
||||
}
|
||||
|
||||
public function isJavascriptRequest(): bool
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
if ($request === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->isXmlHttpRequest()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$request->headers->has('X-Requested-With')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains(strtolower($request->headers->get('X-Requested-With')), 'kimai');
|
||||
}
|
||||
|
||||
public function getBranding(string $config): mixed
|
||||
{
|
||||
@trigger_error('Use "kimai_config" instead of "kimai_context" to access system configurations', E_USER_DEPRECATED);
|
||||
|
||||
return $this->systemConfiguration->find('theme.branding.' . $config);
|
||||
}
|
||||
}
|
||||
@@ -9,112 +9,180 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\User;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Utils\ProfileManager;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class DatatableExtensions extends AbstractExtension
|
||||
final class DatatableExtensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var RequestStack
|
||||
* @var array<string, array<string, array<string, string|bool>>>
|
||||
*/
|
||||
protected $requestStack;
|
||||
private array $dataTables = [];
|
||||
private array $tableNames = [];
|
||||
private ?string $prefix = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cookies = [];
|
||||
|
||||
/**
|
||||
* @param RequestStack $requestStack
|
||||
*/
|
||||
public function __construct(RequestStack $requestStack)
|
||||
public function __construct(private BookmarkRepository $bookmarkRepository, private ProfileManager $profileManager)
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('is_visible_column', [$this, 'isColumnVisible']),
|
||||
new TwigFunction('is_datatable_configured', [$this, 'isDatatableConfigured']),
|
||||
new TwigFunction('initialize_datatable', [$this, 'initializeDatatable']),
|
||||
new TwigFunction('datatable_column_class', [$this, 'getDatatableColumnClass']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dataTable
|
||||
* @return bool
|
||||
*/
|
||||
public function isDatatableConfigured(string $dataTable)
|
||||
private function getDatatableName(string $dataTable): string
|
||||
{
|
||||
$cookie = $this->getVisibilityCookieName($dataTable);
|
||||
if (!\array_key_exists($dataTable, $this->tableNames)) {
|
||||
$this->tableNames[$dataTable] = $this->profileManager->getDatatableName($dataTable, $this->prefix);
|
||||
}
|
||||
|
||||
return $this->requestStack->getCurrentRequest()->cookies->has($cookie);
|
||||
return $this->tableNames[$dataTable];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dataTable
|
||||
* @return string
|
||||
*/
|
||||
protected function getVisibilityCookieName(string $dataTable)
|
||||
public function initializeDatatable(User $user, Session $session, string $dataTable, array $defaultColumns): array
|
||||
{
|
||||
return $dataTable . '_visibility';
|
||||
}
|
||||
if ($this->prefix === null) {
|
||||
$this->prefix = $this->profileManager->getProfileFromSession($session);
|
||||
$dataTable = $this->getDatatableName($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only for datatables, do not use it outside this context.
|
||||
*
|
||||
* @param string $dataTable
|
||||
* @param string $column
|
||||
* @param array $columns
|
||||
* @return bool
|
||||
*/
|
||||
public function isColumnVisible(string $dataTable, string $column, array $columns)
|
||||
{
|
||||
// name handling is spread between here and datatables.html.twig (data_table_column_modal)
|
||||
$cookie = $this->getVisibilityCookieName($dataTable);
|
||||
|
||||
if (!isset($this->cookies[$cookie])) {
|
||||
$visibility = false;
|
||||
if ($this->requestStack->getCurrentRequest()->cookies->has($cookie)) {
|
||||
$visibility = json_decode($this->requestStack->getCurrentRequest()->cookies->get($cookie), true);
|
||||
if (!\array_key_exists($dataTable, $this->dataTables)) {
|
||||
$columns = [];
|
||||
foreach ($defaultColumns as $key => $settings) {
|
||||
$columns[$key] = [
|
||||
'visible' => $this->checkInColumDefinition($defaultColumns, $key),
|
||||
'class' => \array_key_exists($key, $defaultColumns) ? $this->getClass($settings) : ''
|
||||
];
|
||||
// add an auto-generated class
|
||||
$columns[$key]['class'] = trim($columns[$key]['class'] . ' col_' . $key);
|
||||
}
|
||||
$this->cookies[$cookie] = $visibility;
|
||||
}
|
||||
$values = $this->cookies[$cookie];
|
||||
|
||||
if (empty($values) || !\is_array($values)) {
|
||||
return $this->checkInColumDefinition($columns, $column);
|
||||
$bookmark = $this->bookmarkRepository->findBookmark($user, Bookmark::COLUMN_VISIBILITY, $dataTable);
|
||||
if ($bookmark !== null) {
|
||||
$content = $bookmark->getContent();
|
||||
foreach ($content as $key => $value) {
|
||||
if (!\array_key_exists($key, $columns)) {
|
||||
// if a column does not exist any longer, it needs to be skipped, otherwise an error will
|
||||
// be raised while accessing the visible/class keys
|
||||
continue;
|
||||
}
|
||||
$columns[$key]['visible'] = (bool) $value;
|
||||
}
|
||||
|
||||
// disable all columns, which were not bookmarked as visible
|
||||
foreach (array_diff(array_keys($columns), array_keys($content)) as $key) {
|
||||
if (!str_contains($columns[$key]['class'], 'alwaysVisible')) {
|
||||
$columns[$key]['visible'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// now make sure that all columns have proper classes - this might only be applied if a bookmark exists
|
||||
foreach (array_keys($columns) as $key) {
|
||||
if ($columns[$key]['visible']) {
|
||||
$columns[$key]['class'] = $this->makeVisible($columns[$key]['class']);
|
||||
} else {
|
||||
$columns[$key]['class'] = $this->makeHidden($columns[$key]['class']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->dataTables[$dataTable] = $columns;
|
||||
}
|
||||
|
||||
if (!isset($values[$column])) {
|
||||
return $this->checkInColumDefinition($columns, $column);
|
||||
return $this->dataTables[$dataTable];
|
||||
}
|
||||
|
||||
public function getDatatableColumnClass(string $dataTable, string $column): string
|
||||
{
|
||||
$dataTable = $this->getDatatableName($dataTable);
|
||||
|
||||
if (!\array_key_exists($dataTable, $this->dataTables)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($values[$column] === false) {
|
||||
if (!\array_key_exists($column, $this->dataTables[$dataTable])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->dataTables[$dataTable][$column]['class'];
|
||||
}
|
||||
|
||||
private function checkInColumDefinition(array $columns, string $column): bool
|
||||
{
|
||||
if (!\array_key_exists($column, $columns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$tmp = $this->getClass($columns[$column]);
|
||||
|
||||
if (str_contains($tmp, 'alwaysVisible')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = true;
|
||||
if (stripos($tmp, 'd-none') !== false) {
|
||||
$result = false;
|
||||
}
|
||||
|
||||
if (str_contains($tmp, '-table-cell')) {
|
||||
$result = true;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function checkInColumDefinition(array $columns, string $column)
|
||||
private function makeVisible(string $allClasses): string
|
||||
{
|
||||
if (\array_key_exists($column, $columns)) {
|
||||
$tmp = $columns[$column];
|
||||
if (\is_array($tmp)) {
|
||||
$tmp = $tmp['class'];
|
||||
}
|
||||
foreach (explode(' ', $tmp) as $class) {
|
||||
if ($class === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
$newClass = [];
|
||||
foreach (explode(' ', $allClasses) as $class) {
|
||||
if (!str_contains($class, '-none')) {
|
||||
$newClass[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return implode(' ', $newClass);
|
||||
}
|
||||
|
||||
private function makeHidden(string $allClasses): string
|
||||
{
|
||||
$newClass = [];
|
||||
foreach (explode(' ', $allClasses) as $class) {
|
||||
if (!str_contains($class, '-table-cell')) {
|
||||
$newClass[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$newClass = implode(' ', $newClass);
|
||||
|
||||
if (!str_contains($newClass, '-none')) {
|
||||
$newClass .= ' d-none';
|
||||
}
|
||||
|
||||
return $newClass;
|
||||
}
|
||||
|
||||
private function getClass($class): string
|
||||
{
|
||||
if (\is_array($class)) {
|
||||
if (!\array_key_exists('class', $class)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $class['class'];
|
||||
}
|
||||
|
||||
if (!\is_string($class)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,12 @@ use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Multiple Twig extensions: filters and functions
|
||||
*/
|
||||
class Extensions extends AbstractExtension
|
||||
final class Extensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('report_date', [$this, 'formatReportDate']),
|
||||
@@ -41,7 +38,7 @@ class Extensions extends AbstractExtension
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('class_name', [$this, 'getClassName']),
|
||||
@@ -50,10 +47,10 @@ class Extensions extends AbstractExtension
|
||||
];
|
||||
}
|
||||
|
||||
public function getTests()
|
||||
public function getTests(): array
|
||||
{
|
||||
return [
|
||||
new TwigTest('number', function ($value) {
|
||||
new TwigTest('number', function ($value): bool {
|
||||
return !\is_string($value) && is_numeric($value);
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
final class IconExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private static $icons = [
|
||||
'about' => 'fas fa-info-circle',
|
||||
'activity' => 'fas fa-tasks',
|
||||
'admin' => 'fas fa-wrench',
|
||||
'audit' => 'fas fa-history',
|
||||
'avatar' => 'fas fa-user',
|
||||
'back' => 'fas fa-long-arrow-alt-left',
|
||||
'barcode' => 'fas fa-barcode',
|
||||
'bookmark' => 'far fa-star',
|
||||
'bookmarked' => 'fas fa-star',
|
||||
'calendar' => 'far fa-calendar-alt',
|
||||
'clock' => 'far fa-clock',
|
||||
'comment' => 'far fa-comment',
|
||||
'configuration' => 'fas fa-cogs',
|
||||
'copy' => 'far fa-copy',
|
||||
'create' => 'far fa-plus-square',
|
||||
'csv' => 'fas fa-table',
|
||||
'customer' => 'fas fa-user-tie',
|
||||
'dashboard' => 'fas fa-tachometer-alt',
|
||||
'debug' => 'far fa-file-alt',
|
||||
'delete' => 'far fa-trash-alt',
|
||||
'details' => 'fas fa-info-circle',
|
||||
'display' => 'fas fa-layer-group',
|
||||
'doctor' => 'fas fa-medkit',
|
||||
'dot' => 'fas fa-circle',
|
||||
'download' => 'fas fa-download',
|
||||
'duration' => 'far fa-hourglass',
|
||||
'edit' => 'far fa-edit',
|
||||
'end' => 'fas fa-stopwatch',
|
||||
'export' => 'fas fa-file-export',
|
||||
'fax' => 'fas fa-fax',
|
||||
'filter' => 'fas fa-filter',
|
||||
'help' => 'far fa-question-circle',
|
||||
'home' => 'fas fa-home',
|
||||
'invoice' => 'fas fa-file-invoice-dollar',
|
||||
'invoice-template' => 'fas fa-file-signature',
|
||||
'left' => 'fas fa-chevron-left',
|
||||
'list' => 'fas fa-list',
|
||||
'locked' => 'fas fa-lock',
|
||||
'login' => 'fas fa-sign-in-alt',
|
||||
'logout' => 'fas fa-sign-out-alt',
|
||||
'mail' => 'fas fa-envelope-open',
|
||||
'mail-sent' => 'fas fa-paper-plane',
|
||||
'manual' => 'fas fa-book',
|
||||
'mobile' => 'fas fa-mobile',
|
||||
'money' => 'far fa-money-bill-alt',
|
||||
'ods' => 'fas fa-table',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'pin' => 'fas fa-thumbtack',
|
||||
'pdf' => 'fas fa-file-pdf',
|
||||
'pause' => 'fas fa-pause',
|
||||
'pause-small' => 'far fa-pause-circle',
|
||||
'permissions' => 'fas fa-user-lock',
|
||||
'phone' => 'fas fa-phone',
|
||||
'plugin' => 'fas fa-plug',
|
||||
'print' => 'fas fa-print',
|
||||
'profile' => 'fas fa-user-edit',
|
||||
'profile-stats' => 'far fa-chart-bar',
|
||||
'project' => 'fas fa-briefcase',
|
||||
'repeat' => 'fas fa-redo-alt',
|
||||
'reporting' => 'far fa-chart-bar',
|
||||
'right' => 'fas fa-chevron-right',
|
||||
'roles' => 'fas fa-user-shield',
|
||||
'search' => 'fas fa-search',
|
||||
'settings' => 'fas fa-cog',
|
||||
'shop' => 'fas fa-shopping-cart',
|
||||
'start' => 'fas fa-play',
|
||||
'start-small' => 'far fa-play-circle',
|
||||
'stop' => 'fas fa-stop',
|
||||
'stop-small' => 'far fa-stop-circle',
|
||||
'success' => 'fas fa-check',
|
||||
'tag' => 'fas fa-tags',
|
||||
'team' => 'fas fa-users',
|
||||
'timesheet' => 'fas fa-clock',
|
||||
'timesheet-team' => 'fas fa-user-clock',
|
||||
'trash' => 'far fa-trash-alt',
|
||||
'unlocked' => 'fas fa-unlock-alt',
|
||||
'upload' => 'fas fa-upload',
|
||||
'user' => 'fas fa-user',
|
||||
'users' => 'fas fa-user-friends',
|
||||
'visibility' => 'far fa-eye',
|
||||
'warning' => 'fas fa-exclamation-triangle',
|
||||
'weekly-times' => 'fas fa-th',
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('icon', [$this, 'icon']),
|
||||
];
|
||||
}
|
||||
|
||||
public function icon(string $name, string $default = ''): string
|
||||
{
|
||||
return self::$icons[$name] ?? $default;
|
||||
}
|
||||
}
|
||||
@@ -9,81 +9,61 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Constants;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Utils\LocaleFormats;
|
||||
use App\Utils\FormFormatConverter;
|
||||
use App\Utils\JavascriptFormatConverter;
|
||||
use App\Utils\LocaleFormatter;
|
||||
use App\Utils\MomentFormatConverter;
|
||||
use DateTime;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
final class LocaleFormatExtensions extends AbstractExtension
|
||||
final class LocaleFormatExtensions extends AbstractExtension implements LocaleAwareInterface
|
||||
{
|
||||
private $formats;
|
||||
private $security;
|
||||
private ?bool $fdowSunday = null;
|
||||
private ?LocaleFormatter $formatter = null;
|
||||
private ?string $locale = null;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $fdowSunday = null;
|
||||
/**
|
||||
* @var LocaleFormats|null
|
||||
*/
|
||||
private $localeFormats;
|
||||
/**
|
||||
* @var LocaleFormatter|null
|
||||
*/
|
||||
private $formatter;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $locale;
|
||||
private $userFormat;
|
||||
|
||||
public function __construct(LanguageFormattings $formats, Security $security)
|
||||
public function __construct(private LocaleService $localeService, private Security $security)
|
||||
{
|
||||
$this->formats = $formats;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('month_name', [$this, 'monthName']),
|
||||
new TwigFilter('day_name', [$this, 'dayName']),
|
||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||
new TwigFilter('date_full', [$this, 'dateTimeFull']),
|
||||
new TwigFilter('date_full', [$this, 'dateTime']), // deprecated: needs to be kept for invoice and export templates
|
||||
new TwigFilter('date_format', [$this, 'dateFormat']),
|
||||
new TwigFilter('date_weekday', [$this, 'dateWeekday']),
|
||||
new TwigFilter('time', [$this, 'time']),
|
||||
new TwigFilter('hour24', [$this, 'hour24']),
|
||||
new TwigFilter('duration', [$this, 'duration']),
|
||||
new TwigFilter('chart_duration', [$this, 'durationChart']),
|
||||
new TwigFilter('chart_money', [$this, 'moneyChart']),
|
||||
new TwigFilter('duration_decimal', [$this, 'durationDecimal']),
|
||||
new TwigFilter('money', [$this, 'money']),
|
||||
new TwigFilter('currency', [$this, 'currency']),
|
||||
new TwigFilter('country', [$this, 'country']),
|
||||
new TwigFilter('language', [$this, 'language']),
|
||||
new TwigFilter('amount', [$this, 'amount']),
|
||||
new TwigFilter('js_format', [$this, 'convertJavascriptFormat']),
|
||||
new TwigFilter('pattern', [$this, 'convertHtmlPattern']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTests()
|
||||
public function getTests(): array
|
||||
{
|
||||
return [
|
||||
new TwigTest('weekend', [$this, 'isWeekend']),
|
||||
new TwigTest('today', function ($dateTime) {
|
||||
new TwigTest('today', function ($dateTime): bool {
|
||||
if (!$dateTime instanceof \DateTime) {
|
||||
return false;
|
||||
}
|
||||
@@ -97,14 +77,13 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('javascript_configurations', [$this, 'getJavascriptConfiguration']),
|
||||
new TwigFunction('get_format_duration', [$this, 'getDurationFormat']),
|
||||
new TwigFunction('create_date', [$this, 'createDate']),
|
||||
new TwigFunction('locales', [$this, 'getLocales']),
|
||||
new TwigFunction('month_names', [$this, 'getMonthNames']),
|
||||
new TwigFunction('locale_format', [$this, 'getLocaleFormat']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -113,26 +92,16 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
*
|
||||
* @param string $locale
|
||||
*/
|
||||
public function setLocale(string $locale)
|
||||
public function setLocale(string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
$this->formatter = null;
|
||||
$this->localeFormats = null;
|
||||
}
|
||||
|
||||
private function getLocaleFormats(): LocaleFormats
|
||||
{
|
||||
if (null === $this->localeFormats) {
|
||||
$this->localeFormats = new LocaleFormats($this->formats, $this->getLocale());
|
||||
}
|
||||
|
||||
return $this->localeFormats;
|
||||
}
|
||||
|
||||
private function getFormatter(): LocaleFormatter
|
||||
{
|
||||
if (null === $this->formatter) {
|
||||
$this->formatter = new LocaleFormatter($this->formats, $this->getLocale());
|
||||
$this->formatter = new LocaleFormatter($this->localeService, $this->getLocale());
|
||||
}
|
||||
|
||||
return $this->formatter;
|
||||
@@ -147,11 +116,7 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $dateTime
|
||||
* @return bool
|
||||
*/
|
||||
public function isWeekend($dateTime): bool
|
||||
public function isWeekend(DateTime|string|null $dateTime): bool
|
||||
{
|
||||
if (!$dateTime instanceof \DateTime) {
|
||||
return false;
|
||||
@@ -176,43 +141,14 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return ($day === 0 || $day === 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateShort($date)
|
||||
public function dateShort(DateTime|string|null $date): string
|
||||
{
|
||||
return $this->getFormatter()->dateShort($date);
|
||||
return (string) $this->getFormatter()->dateShort($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateTime($date)
|
||||
public function dateTime(DateTime|string|null $date): string
|
||||
{
|
||||
return $this->getFormatter()->dateTime($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @param bool $stripMidnight
|
||||
* @return bool|false|string
|
||||
*/
|
||||
public function dateTimeFull($date, bool $stripMidnight = false)
|
||||
{
|
||||
return $this->getFormatter()->dateTimeFull($date, $this->getUserTimeFormat(), $stripMidnight);
|
||||
}
|
||||
|
||||
private function getUserTimeFormat(): string
|
||||
{
|
||||
if ($this->userFormat === null) {
|
||||
/** @var User|null $user */
|
||||
$user = $this->security->getUser();
|
||||
$this->userFormat = $user !== null ? $user->getTimeFormat() : 'H:i';
|
||||
}
|
||||
|
||||
return $this->userFormat;
|
||||
return (string) $this->getFormatter()->dateTime($date);
|
||||
}
|
||||
|
||||
public function createDate(string $date, ?User $user = null): \DateTime
|
||||
@@ -222,15 +158,9 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return new DateTime($date, new \DateTimeZone($timezone));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @param string $format
|
||||
* @return false|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateFormat($date, string $format)
|
||||
public function dateFormat(DateTime|string|null $date, string $format): string
|
||||
{
|
||||
return $this->getFormatter()->dateFormat($date, $format);
|
||||
return (string) $this->getFormatter()->dateFormat($date, $format);
|
||||
}
|
||||
|
||||
public function dateWeekday(DateTime $date): string
|
||||
@@ -238,13 +168,9 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return $this->dayName($date, true) . ' ' . $this->getFormatter()->dateFormat($date, 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
*/
|
||||
public function time($date)
|
||||
public function time(DateTime|string|null $date): string
|
||||
{
|
||||
return $this->getFormatter()->time($date, $this->getUserTimeFormat());
|
||||
return (string) $this->getFormatter()->time($date);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,44 +202,43 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
return $this->getFormatter()->dayName($dateTime, $short);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $twentyFour
|
||||
* @param mixed $twelveHour
|
||||
* @return mixed
|
||||
*/
|
||||
public function hour24($twentyFour, $twelveHour)
|
||||
{
|
||||
@trigger_error('Twig filter "hour24" is deprecated, use app.user.is24Hour() instead', E_USER_DEPRECATED);
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (null === $user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->is24Hour();
|
||||
}
|
||||
|
||||
public function getJavascriptConfiguration(User $user): array
|
||||
{
|
||||
$converter = new MomentFormatConverter();
|
||||
$format = $this->getLocaleFormats()->getDateTypeFormat();
|
||||
|
||||
return [
|
||||
'formatDuration' => $this->getLocaleFormats()->getDurationFormat(),
|
||||
'formatDate' => $converter->convert($format),
|
||||
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
|
||||
'formatDate' => $this->localeService->getDateFormat($this->locale),
|
||||
'defaultColor' => Constants::DEFAULT_COLOR,
|
||||
'twentyFourHours' => $user->is24Hour(),
|
||||
'updateBrowserTitle' => (bool) $user->getPreferenceValue('theme.update_browser_title'),
|
||||
'twentyFourHours' => $this->localeService->is24Hour($this->locale),
|
||||
'updateBrowserTitle' => (bool) $user->getPreferenceValue('update_browser_title'),
|
||||
'timezone' => $user->getTimezone(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDurationFormat(): string
|
||||
public function getLocaleFormat(string $name): string
|
||||
{
|
||||
@trigger_error('Twig function "get_format_duration()" is deprecated, use "javascript_configurations()" instead.', E_USER_DEPRECATED);
|
||||
$timeFormat = $this->localeService->getTimeFormat($this->locale);
|
||||
$dateFormat = $this->localeService->getDateFormat($this->locale);
|
||||
|
||||
return $this->getLocaleFormats()->getDurationFormat();
|
||||
return match ($name) {
|
||||
'date' => $dateFormat,
|
||||
'time' => $timeFormat,
|
||||
'datetime', 'date-time' => $dateFormat . ' ' . $timeFormat,
|
||||
default => throw new \InvalidArgumentException('Unknown format name: ' . $name),
|
||||
};
|
||||
}
|
||||
|
||||
public function convertJavascriptFormat(string $format): string
|
||||
{
|
||||
$converter = new JavascriptFormatConverter();
|
||||
|
||||
return $converter->convert($format);
|
||||
}
|
||||
|
||||
public function convertHtmlPattern(string $format): string
|
||||
{
|
||||
$converter = new FormFormatConverter();
|
||||
|
||||
return $converter->convertToPattern($format);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,90 +248,47 @@ final class LocaleFormatExtensions extends AbstractExtension
|
||||
* @param bool $decimal
|
||||
* @return string
|
||||
*/
|
||||
public function duration($duration, $decimal = false)
|
||||
public function duration(Timesheet|int|string|null $duration, bool $decimal = false): string
|
||||
{
|
||||
return $this->getFormatter()->duration($duration, $decimal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms seconds into a decimal formatted duration string.
|
||||
*
|
||||
* @param int|Timesheet|null $duration
|
||||
* @return string
|
||||
*/
|
||||
public function durationDecimal($duration)
|
||||
public function durationDecimal(Timesheet|int|string|null $duration): string
|
||||
{
|
||||
return $this->getFormatter()->durationDecimal($duration);
|
||||
}
|
||||
|
||||
public function durationChart($duration): string
|
||||
{
|
||||
return number_format(($duration / 3600), 2, '.', '');
|
||||
}
|
||||
|
||||
public function moneyChart($money): string
|
||||
{
|
||||
return number_format($money, 2, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|float $amount
|
||||
* @return bool|false|string
|
||||
* Transforms seconds into a decimal formatted duration string, for usage with the chart library.
|
||||
*/
|
||||
public function amount($amount)
|
||||
public function durationChart(int|null $duration): string
|
||||
{
|
||||
if ($duration === null) {
|
||||
$duration = 0;
|
||||
}
|
||||
|
||||
return number_format(\floatval($duration / 3600), 2, '.', '');
|
||||
}
|
||||
|
||||
public function moneyChart(int|float|string|null $money): string
|
||||
{
|
||||
if ($money === null) {
|
||||
$money = 0;
|
||||
}
|
||||
|
||||
return number_format(\floatval($money), 2, '.', '');
|
||||
}
|
||||
|
||||
public function amount(null|int|float|string $amount): string
|
||||
{
|
||||
return $this->getFormatter()->amount($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency symbol.
|
||||
*
|
||||
* @param string $currency
|
||||
* @return string
|
||||
*/
|
||||
public function currency($currency)
|
||||
{
|
||||
return $this->getFormatter()->currency($currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
* @return string
|
||||
*/
|
||||
public function language($language)
|
||||
{
|
||||
return $this->getFormatter()->language($language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $country
|
||||
* @return string
|
||||
*/
|
||||
public function country($country)
|
||||
{
|
||||
return $this->getFormatter()->country($country);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $amount
|
||||
* @param string|null $currency
|
||||
* @param bool $withCurrency
|
||||
* @return string
|
||||
*/
|
||||
public function money($amount, ?string $currency = null, bool $withCurrency = true)
|
||||
public function money(float|int|null $amount, ?string $currency = null, bool $withCurrency = true): string
|
||||
{
|
||||
return $this->getFormatter()->money($amount, $currency, $withCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the list of codes of the locales (languages) enabled in the
|
||||
* application and returns an array with the name of each locale written
|
||||
* in its own language (e.g. English, Français, Español, etc.)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLocales()
|
||||
{
|
||||
return $this->getFormatter()->getLocales();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Utils\Pagination;
|
||||
use App\Utils\PaginationView;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Pagerfanta\View\TwitterBootstrap3View;
|
||||
use Pagerfanta\View\ViewInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||
@@ -18,29 +19,20 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class PaginationExtension extends AbstractExtension
|
||||
final class PaginationExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var ViewInterface
|
||||
*/
|
||||
private $view;
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
private ?ViewInterface $view = null;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $router)
|
||||
public function __construct(private UrlGeneratorInterface $router)
|
||||
{
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('pagerfanta', [$this, 'renderPagerfanta'], ['is_safe' => ['html']]),
|
||||
new TwigFunction('pagination', [$this, 'renderPagination'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
@@ -48,40 +40,24 @@ class PaginationExtension extends AbstractExtension
|
||||
private function getView(): ViewInterface
|
||||
{
|
||||
if (null === $this->view) {
|
||||
$this->view = new TwitterBootstrap3View();
|
||||
$this->view = new PaginationView();
|
||||
}
|
||||
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.8
|
||||
*/
|
||||
public function renderPagerfanta(Pagerfanta $pagerfanta, $viewName = null, array $options = [])
|
||||
public function renderPagination(Pagerfanta|Pagination $pager, array $options = []): string
|
||||
{
|
||||
@trigger_error('Twig function pagerfanta() is deprecated and will be removed with 2.0, use pagination() instead', E_USER_DEPRECATED);
|
||||
|
||||
if (\is_array($viewName)) {
|
||||
$options = $viewName;
|
||||
if (!($pager instanceof Pagination)) {
|
||||
@trigger_error('Twig function pagination() needs an instanceof Pagination, Pagerfanta given', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
return $this->renderPagination($pagerfanta, $options);
|
||||
}
|
||||
|
||||
public function renderPagination(Pagerfanta $pagerfanta, array $options = [])
|
||||
{
|
||||
$routeGenerator = $this->createRouteGenerator($options);
|
||||
|
||||
$options['proximity'] = 1;
|
||||
//$options['prev_message'] = '←';
|
||||
//$options['next_message'] = '→';
|
||||
$options['prev_message'] = '<i class="fas fa-chevron-left"></i>';
|
||||
$options['next_message'] = '<i class="fas fa-chevron-right"></i>';
|
||||
|
||||
return $this->getView()->render($pagerfanta, $routeGenerator, $options);
|
||||
return $this->getView()->render($pager, $routeGenerator, $options);
|
||||
}
|
||||
|
||||
private function createRouteGenerator(array $options = [])
|
||||
private function createRouteGenerator(array $options = []): \Closure
|
||||
{
|
||||
$options = array_replace([
|
||||
'routeName' => null,
|
||||
|
||||
@@ -16,22 +16,14 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class EncoreExtension implements RuntimeExtensionInterface, ServiceSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $publicDir;
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $container;
|
||||
private string $publicDir;
|
||||
|
||||
public function __construct(ContainerInterface $container, string $projectDirectory)
|
||||
public function __construct(private ContainerInterface $container, string $projectDirectory)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->publicDir = $projectDirectory . '/public';
|
||||
}
|
||||
|
||||
public static function getSubscribedServices()
|
||||
public static function getSubscribedServices(): array
|
||||
{
|
||||
return [
|
||||
EntrypointLookupInterface::class,
|
||||
|
||||
@@ -15,23 +15,10 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class MarkdownExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @var Markdown
|
||||
*/
|
||||
private $markdown;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $markdownEnabled;
|
||||
private ?bool $markdownEnabled = null;
|
||||
|
||||
public function __construct(Markdown $parser, SystemConfiguration $configuration)
|
||||
public function __construct(private Markdown $markdown, private SystemConfiguration $configuration)
|
||||
{
|
||||
$this->markdown = $parser;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
private function isMarkdownEnabled(): bool
|
||||
|
||||
@@ -16,11 +16,8 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ReportingExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
private $service;
|
||||
|
||||
public function __construct(ReportingService $reportingService)
|
||||
public function __construct(private ReportingService $service)
|
||||
{
|
||||
$this->service = $reportingService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Event\PageActionsEvent;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||
use App\Utils\Color;
|
||||
use App\Utils\FormFormatConverter;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -24,19 +25,8 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ThemeExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $translator;
|
||||
private $configuration;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $randomColors;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TranslatorInterface $translator, SystemConfiguration $configuration)
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->translator = $translator;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,10 +55,8 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
$themeEvent = new PageActionsEvent($user, $payload, $action, $view);
|
||||
|
||||
$eventName = 'actions.' . $action;
|
||||
|
||||
if ($this->eventDispatcher->hasListeners($eventName)) {
|
||||
$this->eventDispatcher->dispatch($themeEvent, $eventName);
|
||||
if ($this->eventDispatcher->hasListeners($themeEvent->getEventName())) {
|
||||
$this->eventDispatcher->dispatch($themeEvent, $themeEvent->getEventName());
|
||||
}
|
||||
|
||||
return $themeEvent;
|
||||
@@ -80,14 +68,19 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
return $event->getTranslations();
|
||||
$all = [];
|
||||
foreach ($event->getTranslations() as $key => $translation) {
|
||||
$all[$key] = $this->translator->trans($translation[0], [], $translation[1]);
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getProgressbarClass(float $percent, ?bool $reverseColors = false): string
|
||||
{
|
||||
$colors = ['xl' => 'progress-bar-danger', 'l' => 'progress-bar-warning', 'm' => 'progress-bar-success', 's' => 'progress-bar-primary', 'e' => 'progress-bar-info'];
|
||||
$colors = ['xl' => 'bg-red', 'l' => 'bg-warning', 'm' => 'bg-green', 's' => 'bg-green', 'e' => ''];
|
||||
if (true === $reverseColors) {
|
||||
$colors = ['s' => 'progress-bar-danger', 'm' => 'progress-bar-warning', 'l' => 'progress-bar-success', 'xl' => 'progress-bar-primary', 'e' => 'progress-bar-info'];
|
||||
$colors = ['s' => 'bg-red', 'm' => 'bg-warning', 'l' => 'bg-green', 'xl' => 'bg-green', 'e' => ''];
|
||||
}
|
||||
|
||||
if ($percent > 90) {
|
||||
@@ -107,58 +100,44 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function generateTitle(?string $prefix = null, string $delimiter = ' – '): string
|
||||
{
|
||||
$title = $this->configuration->getBrandingTitle();
|
||||
if (null === $title || \strlen($title) === 0) {
|
||||
$title = Constants::SOFTWARE;
|
||||
}
|
||||
|
||||
return ($prefix ?? '') . $title . $delimiter . $this->translator->trans('time_tracking', [], 'messages');
|
||||
return ($prefix ?? '') . Constants::SOFTWARE . $delimiter . $this->translator->trans('time_tracking', [], 'messages');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
* @deprecated since 1.15
|
||||
*/
|
||||
public function getThemeConfig(string $name)
|
||||
{
|
||||
@trigger_error('The twig function "theme_config" was deprecated with 1.15, replace it with the global "kimai_config" variable.', E_USER_DEPRECATED);
|
||||
|
||||
switch ($name) {
|
||||
case 'auto_reload_datatable':
|
||||
@trigger_error('The configuration auto_reload_datatable is deprecated and was removed with 1.4', E_USER_DEPRECATED);
|
||||
|
||||
return false;
|
||||
|
||||
case 'soft_limit':
|
||||
return $this->configuration->getTimesheetActiveEntriesHardLimit();
|
||||
|
||||
default:
|
||||
$name = 'theme.' . $name;
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->configuration->find($name);
|
||||
}
|
||||
|
||||
public function colorize(?string $color, ?string $identifier = null, ?string $fallback = null): string
|
||||
public function colorize(?string $color, ?string $identifier = null): string
|
||||
{
|
||||
if ($color !== null) {
|
||||
return $color;
|
||||
}
|
||||
|
||||
if ($this->randomColors === null) {
|
||||
$this->randomColors = $this->configuration->isThemeRandomColors();
|
||||
return (new Color())->getRandom($identifier);
|
||||
}
|
||||
|
||||
public function getTimePresets(string $timezone, string $format): array
|
||||
{
|
||||
$converter = new FormFormatConverter();
|
||||
$format = $converter->convert($format);
|
||||
|
||||
$intervalMinutes = $this->configuration->getTimesheetIncrementMinutes();
|
||||
|
||||
if ($intervalMinutes < 5) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->randomColors) {
|
||||
return (new Color())->getRandom($identifier);
|
||||
$maxMinutes = 24 * 60 - $intervalMinutes;
|
||||
|
||||
$date = new \DateTime('now', new \DateTimeZone($timezone));
|
||||
$date->setTime(0, 0, 0);
|
||||
|
||||
$presets = [
|
||||
$date->format($format)
|
||||
];
|
||||
|
||||
for ($minutes = $intervalMinutes; $minutes <= $maxMinutes; $minutes += $intervalMinutes) {
|
||||
$date->modify('+' . $intervalMinutes . ' minutes');
|
||||
|
||||
$presets[] = $date->format($format);
|
||||
}
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return Constants::DEFAULT_COLOR;
|
||||
return $presets;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,36 @@
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\FavoriteTimesheet;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\FavoriteRecordService;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class TimesheetExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
public function __construct(private TimesheetRepository $repository, private FavoriteRecordService $favoriteRecordService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function activeEntries(User $user): array
|
||||
/**
|
||||
* @param User $user
|
||||
* @param bool $ticktac
|
||||
* @return array<Timesheet>
|
||||
*/
|
||||
public function activeEntries(User $user, bool $ticktac = true): array
|
||||
{
|
||||
return $this->repository->getActiveEntries($user);
|
||||
return $this->repository->getActiveEntries($user, $ticktac);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param int $limit
|
||||
* @return array<FavoriteTimesheet>
|
||||
*/
|
||||
public function favoriteEntries(User $user, int $limit = 5): array
|
||||
{
|
||||
return $this->favoriteRecordService->favoriteEntries($user, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,18 @@
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class WidgetExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @var WidgetService
|
||||
*/
|
||||
private $service;
|
||||
|
||||
public function __construct(WidgetService $service)
|
||||
public function __construct(private WidgetService $service, private Security $security)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,10 +29,10 @@ final class WidgetExtension implements RuntimeExtensionInterface
|
||||
* @return string
|
||||
* @throws WidgetException
|
||||
*/
|
||||
public function renderWidget($widget, array $options = [])
|
||||
public function renderWidget(Environment $environment, $widget, array $options = []): string
|
||||
{
|
||||
if (!($widget instanceof WidgetInterface) && !\is_string($widget)) {
|
||||
throw new \InvalidArgumentException('Widget must either implement WidgetInterface or be a string');
|
||||
throw new \InvalidArgumentException('Widget must be either a WidgetInterface or a string');
|
||||
}
|
||||
|
||||
if (\is_string($widget)) {
|
||||
@@ -46,8 +43,18 @@ final class WidgetExtension implements RuntimeExtensionInterface
|
||||
$widget = $this->service->getWidget($widget);
|
||||
}
|
||||
|
||||
$renderer = $this->service->findRenderer($widget);
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof User) {
|
||||
$widget->setUser($user);
|
||||
}
|
||||
|
||||
return $renderer->render($widget, $options);
|
||||
$options = $widget->getOptions($options);
|
||||
|
||||
return $environment->render($widget->getTemplateName(), [
|
||||
'data' => $widget->getData($options),
|
||||
'options' => $options,
|
||||
'title' => $widget->getTitle(),
|
||||
'widget' => $widget,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ use App\Twig\Runtime\MarkdownExtension;
|
||||
use App\Twig\Runtime\ThemeExtension;
|
||||
use App\Twig\Runtime\TimesheetExtension;
|
||||
use App\Twig\Runtime\WidgetExtension;
|
||||
use KevinPapst\TablerBundle\Twig\RuntimeExtension;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class RuntimeExtensions extends AbstractExtension
|
||||
final class RuntimeExtensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('trigger', [ThemeExtension::class, 'trigger'], ['needs_environment' => true]),
|
||||
@@ -31,17 +29,16 @@ class RuntimeExtensions extends AbstractExtension
|
||||
new TwigFunction('get_title', [ThemeExtension::class, 'generateTitle']),
|
||||
new TwigFunction('progressbar_color', [ThemeExtension::class, 'getProgressbarClass']),
|
||||
new TwigFunction('javascript_translations', [ThemeExtension::class, 'getJavascriptTranslations']),
|
||||
new TwigFunction('theme_config', [ThemeExtension::class, 'getThemeConfig']),
|
||||
new TwigFunction('form_time_presets', [ThemeExtension::class, 'getTimePresets']),
|
||||
new TwigFunction('active_timesheets', [TimesheetExtension::class, 'activeEntries']),
|
||||
new TwigFunction('favorite_timesheets', [TimesheetExtension::class, 'favoriteEntries']),
|
||||
new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']),
|
||||
new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html']]),
|
||||
new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html'], 'needs_environment' => true]),
|
||||
new TwigFunction('icon', [RuntimeExtension::class, 'createIcon'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('md2html', [MarkdownExtension::class, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
@@ -49,6 +46,7 @@ class RuntimeExtensions extends AbstractExtension
|
||||
new TwigFilter('comment2html', [MarkdownExtension::class, 'commentContent'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('comment1line', [MarkdownExtension::class, 'commentOneLiner'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('colorize', [ThemeExtension::class, 'colorize']),
|
||||
new TwigFilter('icon', [RuntimeExtension::class, 'icon']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user