@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'])]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ final class InvoiceCalculatorType extends AbstractType
|
||||
return $renderer;
|
||||
},
|
||||
'translation_domain' => 'invoice-calculator',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ final class InvoiceRendererType extends AbstractType
|
||||
};
|
||||
},
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ final class SkinType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'search' => false,
|
||||
'required' => true,
|
||||
'choices' => self::THEMES,
|
||||
]);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user