Release 2.0.16 (#3990)

https://github.com/kimai/kimai/pull/3990
This commit is contained in:
Kevin Papst
2023-04-27 18:48:31 +02:00
committed by GitHub
parent 844062b90e
commit 01e26dca9e
28 changed files with 198 additions and 102 deletions

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.15';
public const VERSION = '2.0.16';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20015;
public const VERSION_ID = 20016;
/**
* The software name
*/

View File

@@ -406,7 +406,7 @@ final class ProfileController extends AbstractController
return $this->redirectToRoute('user_profile_2fa', ['username' => $profile->getUserIdentifier()]);
}
#[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])]
#[Route(path: '/{username}/totp-qr-code', name: 'user_profile_2fa_image', methods: ['GET'])]
#[IsGranted('2fa', 'profile')]
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
{

View File

@@ -9,15 +9,10 @@
namespace App\DependencyInjection\Compiler;
use App\Export\Renderer\HtmlRenderer;
use App\Export\Renderer\HtmlRendererFactory;
use App\Export\Renderer\PDFRenderer;
use App\Export\Renderer\PdfRendererFactory;
use App\Export\ServiceExport;
use App\Kernel;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
@@ -44,44 +39,15 @@ final class ExportServiceCompilerPass implements CompilerPassInterface
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
}
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
foreach ($container->getParameter('kimai.export.documents') as $exportPath) {
if (!is_dir($path . $exportPath)) {
continue;
}
foreach (glob($path . $exportPath . '/*.html.twig') as $htmlTpl) {
$tplName = basename($htmlTpl);
if (stripos($tplName, '-bundle') !== false) {
$exportDocuments = $container->getParameter('kimai.export.documents');
if (\is_array($exportDocuments)) {
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
foreach ($exportDocuments as $exportPath) {
if (!is_dir($path . $exportPath)) {
continue;
}
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
$factoryDefinition = new Definition(HtmlRenderer::class);
$factoryDefinition->addArgument($tplName);
$factoryDefinition->addArgument($tplName);
$factoryDefinition->setFactory([new Reference(HtmlRendererFactory::class), 'create']);
$container->setDefinition($serviceId, $factoryDefinition);
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
}
foreach (glob($path . $exportPath . '/*.pdf.twig') as $pdfHtml) {
$tplName = basename($pdfHtml);
if (stripos($tplName, '-bundle') !== false) {
continue;
}
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
$factoryDefinition = new Definition(PDFRenderer::class);
$factoryDefinition->addArgument($tplName);
$factoryDefinition->addArgument($tplName);
$factoryDefinition->setFactory([new Reference(PdfRendererFactory::class), 'create']);
$container->setDefinition($serviceId, $factoryDefinition);
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
$definition->addMethodCall('addDirectory', [realpath($path . $exportPath)]);
}
}
}

View File

@@ -184,7 +184,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
/**
* Internal property used to determine whether the billable field should be calculated automatically.
*/
private string $billableMode = self::BILLABLE_DEFAULT;
#[Assert\NotNull]
private ?string $billableMode = self::BILLABLE_DEFAULT;
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
#[Assert\NotNull]
private ?string $category = self::WORK;
@@ -552,12 +553,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
return $this;
}
public function getBillableMode(): string
public function getBillableMode(): ?string
{
return $this->billableMode;
}
public function setBillableMode(string $billableMode): void
public function setBillableMode(?string $billableMode): void
{
$this->billableMode = $billableMode;
}

View File

@@ -158,6 +158,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
private ?bool $isAllowedToSeeAllData = null;
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]

View File

@@ -11,28 +11,56 @@ namespace App\Export;
use App\Entity\ExportableItem;
use App\Event\ExportItemsQueryEvent;
use App\Export\Renderer\HtmlRendererFactory;
use App\Export\Renderer\PdfRendererFactory;
use App\Repository\Query\ExportQuery;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class ServiceExport
{
/**
* @var array<int, string>
*/
private array $documentDirs = [];
/**
* @var ExportRendererInterface[]
*/
private $renderer = [];
private array $renderer = [];
/**
* @var TimesheetExportInterface[]
*/
private $timesheetExporter = [];
private array $timesheetExporter = [];
/**
* @var ExportRepositoryInterface[]
*/
private $repositories = [];
private array $repositories = [];
public function __construct(private EventDispatcherInterface $eventDispatcher)
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private HtmlRendererFactory $htmlRendererFactory,
private PdfRendererFactory $pdfRendererFactory
)
{
}
/**
* @CloudRequired
*/
public function addDirectory(string $directory): void
{
$this->documentDirs[] = $directory;
}
/**
* @CloudRequired
*/
public function removeDirectory(string $directory): void
{
if (($key = array_search($directory, $this->documentDirs, true)) !== false) {
unset($this->documentDirs[$key]);
}
}
public function addRenderer(ExportRendererInterface $renderer): void
{
$this->renderer[] = $renderer;
@@ -43,12 +71,44 @@ final class ServiceExport
*/
public function getRenderer(): array
{
return $this->renderer;
$renderer = [];
foreach ($this->documentDirs as $exportPath) {
if (!is_dir($exportPath)) {
continue;
}
$htmlTemplates = glob($exportPath . '/*.html.twig');
if (\is_array($htmlTemplates)) {
foreach ($htmlTemplates as $htmlTpl) {
$tplName = basename($htmlTpl);
if (stripos($tplName, '-bundle') !== false) {
continue;
}
$renderer[] = $this->htmlRendererFactory->create($tplName, $tplName);
}
}
$pdfTemplates = glob($exportPath . '/*.pdf.twig');
if (\is_array($pdfTemplates)) {
foreach ($pdfTemplates as $pdfTpl) {
$tplName = basename($pdfTpl);
if (stripos($tplName, '-bundle') !== false) {
continue;
}
$renderer[] = $this->pdfRendererFactory->create($tplName, $tplName);
}
}
}
return array_merge($this->renderer, $renderer);
}
public function getRendererById(string $id): ?ExportRendererInterface
{
foreach ($this->renderer as $renderer) {
foreach ($this->getRenderer() as $renderer) {
if ($renderer->getId() === $id) {
return $renderer;
}

View File

@@ -57,6 +57,13 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
$extendedOptions['data-disable-search'] = 1;
}
// there is a very weird logic in vendor/symfony/twig-bridge/Resources/views/Form/form_div_layout.html.twig
// in block "block choice_widget_collapsed" that resets "{% set required = false %}", so we fake it into the select
if (true === $options['required'] && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
$extendedOptions['required'] = 'required';
$extendedOptions['placeholder'] = '';
}
$view->vars['attr'] = array_merge($view->vars['attr'], $extendedOptions);
}

View File

@@ -31,6 +31,7 @@ class ProjectEditForm extends AbstractType
$customer = null;
$isNew = false;
$options['currency'] = null;
$customerOptions = [];
if (isset($options['data'])) {
/** @var Project $entry */
@@ -40,6 +41,11 @@ class ProjectEditForm extends AbstractType
if (null !== $entry->getCustomer()) {
$customer = $entry->getCustomer();
$options['currency'] = $customer->getCurrency();
if (!$customer->isVisible()) {
// force visibility, see https://github.com/kimai/kimai/issues/3985
$customerOptions['pre_select_customer'] = true;
}
}
}
@@ -83,11 +89,11 @@ class ProjectEditForm extends AbstractType
'required' => false,
'force_time' => 'end',
]))
->add('customer', CustomerType::class, [
->add('customer', CustomerType::class, array_merge([
'placeholder' => ($isNew && null === $customer) ? '' : false,
'customers' => $customer,
'query_builder_for_user' => true,
])
], $customerOptions))
->add('globalActivities', YesNoType::class, [
'label' => 'globalActivities',
'help' => 'help.globalActivities'

View File

@@ -133,19 +133,13 @@ trait ToolbarFormTrait
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'visible'): void
{
$builder->add('visibility', VisibilityType::class, [
'required' => false,
'placeholder' => null,
'label' => $label,
'search' => false
]);
}
protected function addPageSizeChoice(FormBuilderInterface $builder): void
{
$builder->add('pageSize', PageSizeType::class, [
'required' => false,
'search' => false
]);
$builder->add('pageSize', PageSizeType::class);
}
protected function addUserRoleChoice(FormBuilderInterface $builder): void
@@ -332,7 +326,6 @@ trait ToolbarFormTrait
'asc' => BaseQuery::ORDER_ASC,
'desc' => BaseQuery::ORDER_DESC
],
'search' => false,
]);
}
@@ -346,7 +339,6 @@ trait ToolbarFormTrait
$builder->add('orderBy', ChoiceType::class, [
'label' => 'orderBy',
'choices' => $all,
'search' => false,
]);
}
@@ -367,9 +359,6 @@ trait ToolbarFormTrait
{
$builder->add('state', ChoiceType::class, [
'label' => 'entryState',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'all' => TimesheetQuery::STATE_ALL,
'entryState.running' => TimesheetQuery::STATE_RUNNING,
@@ -382,9 +371,6 @@ trait ToolbarFormTrait
{
$builder->add('exported', ChoiceType::class, [
'label' => 'exported',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'all' => TimesheetQuery::STATE_ALL,
'yes' => TimesheetQuery::STATE_EXPORTED,
@@ -395,10 +381,6 @@ trait ToolbarFormTrait
protected function addBillableChoice(FormBuilderInterface $builder): void
{
$builder->add('billable', BillableSearchType::class, [
'required' => false,
'placeholder' => null,
'search' => false,
]);
$builder->add('billable', BillableSearchType::class);
}
}

View File

@@ -63,11 +63,17 @@ final class CustomerType extends AbstractType
'ignore_customer' => null,
// @var Customer|Customer[]|null
'customers' => null,
'pre_select_customer' => false,
]);
$resolver->setDefault('query_builder', function (Options $options) {
return function (CustomerRepository $repo) use ($options) {
$query = new CustomerFormTypeQuery($options['customers']);
if (true === $options['pre_select_customer']) {
$query->setAllowCustomerPreselect(true);
}
if (true === $options['query_builder_for_user']) {
$query->setUser($options['user']);
}

View File

@@ -37,7 +37,6 @@ final class InvoiceCalculatorType extends AbstractType
return $renderer;
},
'translation_domain' => 'invoice-calculator',
'search' => false,
]);
}

View File

@@ -47,7 +47,6 @@ final class InvoiceRendererType extends AbstractType
};
},
'translation_domain' => 'invoice-renderer',
'search' => false,
]);
}

View File

@@ -26,6 +26,7 @@ final class SkinType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'search' => false,
'required' => true,
'choices' => self::THEMES,
]);

View File

@@ -45,7 +45,7 @@ final class InvoiceDocumentRepository
*/
public function removeDirectory(string $directory): void
{
if (($key = array_search($directory, $this->documentDirs)) !== false) {
if (($key = array_search($directory, $this->documentDirs, true)) !== false) {
unset($this->documentDirs[$key]);
}
}