3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@
|
|||||||
rector.php
|
rector.php
|
||||||
phpstan.sh
|
phpstan.sh
|
||||||
|
|
||||||
|
# custom apache rules e.g. to deactivate ioncube loader
|
||||||
|
public/.user.ini
|
||||||
|
|
||||||
# for symfony local webserver
|
# for symfony local webserver
|
||||||
php.ini
|
php.ini
|
||||||
.php-version
|
.php-version
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
|||||||
let plugins = ['change_listener'];
|
let plugins = ['change_listener'];
|
||||||
|
|
||||||
const isMultiple = node.multiple !== undefined && node.multiple === true;
|
const isMultiple = node.multiple !== undefined && node.multiple === true;
|
||||||
|
const isRequired = node.required !== undefined && node.required === true;
|
||||||
|
|
||||||
|
if (isRequired) {
|
||||||
|
plugins.push('no_backspace_delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiple) {
|
||||||
|
plugins.push('remove_button');
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const isOrdering = false;
|
const isOrdering = false;
|
||||||
@@ -61,13 +70,9 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (isMultiple) {
|
|
||||||
plugins.push('remove_button');
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
lockOptgroupOrder: true,
|
lockOptgroupOrder: true,
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: !isRequired,
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
// if there are more than X entries, the other ones are hidden and can only be found
|
// if there are more than X entries, the other ones are hidden and can only be found
|
||||||
// by typing some characters to trigger the internal option search
|
// by typing some characters to trigger the internal option search
|
||||||
|
|||||||
@@ -1685,11 +1685,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/DependencyInjection/AppExtension.php
|
path: src/DependencyInjection/AppExtension.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: src/DependencyInjection/Compiler/ExportServiceCompilerPass.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\DependencyInjection\\\\Configuration\\:\\:getProjectNode\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\DependencyInjection\\\\Configuration\\:\\:getProjectNode\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
2
public/build/app.902831bc.js
Normal file
2
public/build/app.902831bc.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/app.cd312ca8.js"
|
"/build/app.902831bc.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.b10b16e5.css"
|
"/build/app.b10b16e5.css"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||||
"/build/app.cd312ca8.js": "sha384-lS1VK+quldXkQU0DjsipiX8sXWvojX/NY0+kAUHFh69HlZZG36ubhkNbcV0crl35",
|
"/build/app.902831bc.js": "sha384-t6GwvEu6PBNMxOHW4JHSScxFmGE7tGus/IZoBBZHrGFdtDZtKjweNAqJ00VmSnSp",
|
||||||
"/build/app.b10b16e5.css": "sha384-dwEgmHAQRm4PP7XTYkJfIoB+Iumd+0WuH5FyoC/+qC3WQ/e1gCTmlx9M2mIBxPuK",
|
"/build/app.b10b16e5.css": "sha384-dwEgmHAQRm4PP7XTYkJfIoB+Iumd+0WuH5FyoC/+qC3WQ/e1gCTmlx9M2mIBxPuK",
|
||||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "/build/app.b10b16e5.css",
|
"build/app.css": "/build/app.b10b16e5.css",
|
||||||
"build/app.js": "/build/app.cd312ca8.js",
|
"build/app.js": "/build/app.902831bc.js",
|
||||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||||
"build/invoice.css": "/build/invoice.d0ae14e1.css",
|
"build/invoice.css": "/build/invoice.d0ae14e1.css",
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.15';
|
public const VERSION = '2.0.16';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20015;
|
public const VERSION_ID = 20016;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ final class ProfileController extends AbstractController
|
|||||||
return $this->redirectToRoute('user_profile_2fa', ['username' => $profile->getUserIdentifier()]);
|
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')]
|
#[IsGranted('2fa', 'profile')]
|
||||||
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
|
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,15 +9,10 @@
|
|||||||
|
|
||||||
namespace App\DependencyInjection\Compiler;
|
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\Export\ServiceExport;
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Definition;
|
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,44 +39,15 @@ final class ExportServiceCompilerPass implements CompilerPassInterface
|
|||||||
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
|
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
$exportDocuments = $container->getParameter('kimai.export.documents');
|
||||||
foreach ($container->getParameter('kimai.export.documents') as $exportPath) {
|
if (\is_array($exportDocuments)) {
|
||||||
if (!is_dir($path . $exportPath)) {
|
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||||
continue;
|
foreach ($exportDocuments as $exportPath) {
|
||||||
}
|
if (!is_dir($path . $exportPath)) {
|
||||||
|
|
||||||
foreach (glob($path . $exportPath . '/*.html.twig') as $htmlTpl) {
|
|
||||||
$tplName = basename($htmlTpl);
|
|
||||||
if (stripos($tplName, '-bundle') !== false) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
|
$definition->addMethodCall('addDirectory', [realpath($path . $exportPath)]);
|
||||||
|
|
||||||
$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)]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
/**
|
/**
|
||||||
* Internal property used to determine whether the billable field should be calculated automatically.
|
* 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'])]
|
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private ?string $category = self::WORK;
|
private ?string $category = self::WORK;
|
||||||
@@ -552,12 +553,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBillableMode(): string
|
public function getBillableMode(): ?string
|
||||||
{
|
{
|
||||||
return $this->billableMode;
|
return $this->billableMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBillableMode(string $billableMode): void
|
public function setBillableMode(?string $billableMode): void
|
||||||
{
|
{
|
||||||
$this->billableMode = $billableMode;
|
$this->billableMode = $billableMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
private ?bool $isAllowedToSeeAllData = null;
|
private ?bool $isAllowedToSeeAllData = null;
|
||||||
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
|
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
|
||||||
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
|
#[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'])]
|
#[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
|
|||||||
@@ -11,28 +11,56 @@ namespace App\Export;
|
|||||||
|
|
||||||
use App\Entity\ExportableItem;
|
use App\Entity\ExportableItem;
|
||||||
use App\Event\ExportItemsQueryEvent;
|
use App\Event\ExportItemsQueryEvent;
|
||||||
|
use App\Export\Renderer\HtmlRendererFactory;
|
||||||
|
use App\Export\Renderer\PdfRendererFactory;
|
||||||
use App\Repository\Query\ExportQuery;
|
use App\Repository\Query\ExportQuery;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
final class ServiceExport
|
final class ServiceExport
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private array $documentDirs = [];
|
||||||
/**
|
/**
|
||||||
* @var ExportRendererInterface[]
|
* @var ExportRendererInterface[]
|
||||||
*/
|
*/
|
||||||
private $renderer = [];
|
private array $renderer = [];
|
||||||
/**
|
/**
|
||||||
* @var TimesheetExportInterface[]
|
* @var TimesheetExportInterface[]
|
||||||
*/
|
*/
|
||||||
private $timesheetExporter = [];
|
private array $timesheetExporter = [];
|
||||||
/**
|
/**
|
||||||
* @var ExportRepositoryInterface[]
|
* @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
|
public function addRenderer(ExportRendererInterface $renderer): void
|
||||||
{
|
{
|
||||||
$this->renderer[] = $renderer;
|
$this->renderer[] = $renderer;
|
||||||
@@ -43,12 +71,44 @@ final class ServiceExport
|
|||||||
*/
|
*/
|
||||||
public function getRenderer(): array
|
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
|
public function getRendererById(string $id): ?ExportRendererInterface
|
||||||
{
|
{
|
||||||
foreach ($this->renderer as $renderer) {
|
foreach ($this->getRenderer() as $renderer) {
|
||||||
if ($renderer->getId() === $id) {
|
if ($renderer->getId() === $id) {
|
||||||
return $renderer;
|
return $renderer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
|||||||
$extendedOptions['data-disable-search'] = 1;
|
$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->vars['attr'] = array_merge($view->vars['attr'], $extendedOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class ProjectEditForm extends AbstractType
|
|||||||
$customer = null;
|
$customer = null;
|
||||||
$isNew = false;
|
$isNew = false;
|
||||||
$options['currency'] = null;
|
$options['currency'] = null;
|
||||||
|
$customerOptions = [];
|
||||||
|
|
||||||
if (isset($options['data'])) {
|
if (isset($options['data'])) {
|
||||||
/** @var Project $entry */
|
/** @var Project $entry */
|
||||||
@@ -40,6 +41,11 @@ class ProjectEditForm extends AbstractType
|
|||||||
if (null !== $entry->getCustomer()) {
|
if (null !== $entry->getCustomer()) {
|
||||||
$customer = $entry->getCustomer();
|
$customer = $entry->getCustomer();
|
||||||
$options['currency'] = $customer->getCurrency();
|
$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,
|
'required' => false,
|
||||||
'force_time' => 'end',
|
'force_time' => 'end',
|
||||||
]))
|
]))
|
||||||
->add('customer', CustomerType::class, [
|
->add('customer', CustomerType::class, array_merge([
|
||||||
'placeholder' => ($isNew && null === $customer) ? '' : false,
|
'placeholder' => ($isNew && null === $customer) ? '' : false,
|
||||||
'customers' => $customer,
|
'customers' => $customer,
|
||||||
'query_builder_for_user' => true,
|
'query_builder_for_user' => true,
|
||||||
])
|
], $customerOptions))
|
||||||
->add('globalActivities', YesNoType::class, [
|
->add('globalActivities', YesNoType::class, [
|
||||||
'label' => 'globalActivities',
|
'label' => 'globalActivities',
|
||||||
'help' => 'help.globalActivities'
|
'help' => 'help.globalActivities'
|
||||||
|
|||||||
@@ -133,19 +133,13 @@ trait ToolbarFormTrait
|
|||||||
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'visible'): void
|
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'visible'): void
|
||||||
{
|
{
|
||||||
$builder->add('visibility', VisibilityType::class, [
|
$builder->add('visibility', VisibilityType::class, [
|
||||||
'required' => false,
|
|
||||||
'placeholder' => null,
|
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
'search' => false
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addPageSizeChoice(FormBuilderInterface $builder): void
|
protected function addPageSizeChoice(FormBuilderInterface $builder): void
|
||||||
{
|
{
|
||||||
$builder->add('pageSize', PageSizeType::class, [
|
$builder->add('pageSize', PageSizeType::class);
|
||||||
'required' => false,
|
|
||||||
'search' => false
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addUserRoleChoice(FormBuilderInterface $builder): void
|
protected function addUserRoleChoice(FormBuilderInterface $builder): void
|
||||||
@@ -332,7 +326,6 @@ trait ToolbarFormTrait
|
|||||||
'asc' => BaseQuery::ORDER_ASC,
|
'asc' => BaseQuery::ORDER_ASC,
|
||||||
'desc' => BaseQuery::ORDER_DESC
|
'desc' => BaseQuery::ORDER_DESC
|
||||||
],
|
],
|
||||||
'search' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +339,6 @@ trait ToolbarFormTrait
|
|||||||
$builder->add('orderBy', ChoiceType::class, [
|
$builder->add('orderBy', ChoiceType::class, [
|
||||||
'label' => 'orderBy',
|
'label' => 'orderBy',
|
||||||
'choices' => $all,
|
'choices' => $all,
|
||||||
'search' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,9 +359,6 @@ trait ToolbarFormTrait
|
|||||||
{
|
{
|
||||||
$builder->add('state', ChoiceType::class, [
|
$builder->add('state', ChoiceType::class, [
|
||||||
'label' => 'entryState',
|
'label' => 'entryState',
|
||||||
'required' => false,
|
|
||||||
'placeholder' => null,
|
|
||||||
'search' => false,
|
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'all' => TimesheetQuery::STATE_ALL,
|
'all' => TimesheetQuery::STATE_ALL,
|
||||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||||
@@ -382,9 +371,6 @@ trait ToolbarFormTrait
|
|||||||
{
|
{
|
||||||
$builder->add('exported', ChoiceType::class, [
|
$builder->add('exported', ChoiceType::class, [
|
||||||
'label' => 'exported',
|
'label' => 'exported',
|
||||||
'required' => false,
|
|
||||||
'placeholder' => null,
|
|
||||||
'search' => false,
|
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'all' => TimesheetQuery::STATE_ALL,
|
'all' => TimesheetQuery::STATE_ALL,
|
||||||
'yes' => TimesheetQuery::STATE_EXPORTED,
|
'yes' => TimesheetQuery::STATE_EXPORTED,
|
||||||
@@ -395,10 +381,6 @@ trait ToolbarFormTrait
|
|||||||
|
|
||||||
protected function addBillableChoice(FormBuilderInterface $builder): void
|
protected function addBillableChoice(FormBuilderInterface $builder): void
|
||||||
{
|
{
|
||||||
$builder->add('billable', BillableSearchType::class, [
|
$builder->add('billable', BillableSearchType::class);
|
||||||
'required' => false,
|
|
||||||
'placeholder' => null,
|
|
||||||
'search' => false,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,17 @@ final class CustomerType extends AbstractType
|
|||||||
'ignore_customer' => null,
|
'ignore_customer' => null,
|
||||||
// @var Customer|Customer[]|null
|
// @var Customer|Customer[]|null
|
||||||
'customers' => null,
|
'customers' => null,
|
||||||
|
'pre_select_customer' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$resolver->setDefault('query_builder', function (Options $options) {
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
return function (CustomerRepository $repo) use ($options) {
|
return function (CustomerRepository $repo) use ($options) {
|
||||||
$query = new CustomerFormTypeQuery($options['customers']);
|
$query = new CustomerFormTypeQuery($options['customers']);
|
||||||
|
|
||||||
|
if (true === $options['pre_select_customer']) {
|
||||||
|
$query->setAllowCustomerPreselect(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (true === $options['query_builder_for_user']) {
|
if (true === $options['query_builder_for_user']) {
|
||||||
$query->setUser($options['user']);
|
$query->setUser($options['user']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ final class InvoiceCalculatorType extends AbstractType
|
|||||||
return $renderer;
|
return $renderer;
|
||||||
},
|
},
|
||||||
'translation_domain' => 'invoice-calculator',
|
'translation_domain' => 'invoice-calculator',
|
||||||
'search' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ final class InvoiceRendererType extends AbstractType
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
'translation_domain' => 'invoice-renderer',
|
'translation_domain' => 'invoice-renderer',
|
||||||
'search' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class SkinType extends AbstractType
|
|||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
|
'search' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'choices' => self::THEMES,
|
'choices' => self::THEMES,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class InvoiceDocumentRepository
|
|||||||
*/
|
*/
|
||||||
public function removeDirectory(string $directory): void
|
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]);
|
unset($this->documentDirs[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$content = $client->getResponse()->getContent();
|
$content = $client->getResponse()->getContent();
|
||||||
self::assertNotFalse($content);
|
self::assertNotFalse($content);
|
||||||
|
|
||||||
$imgUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
$imgUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||||
$this->assertStringContainsString('<img src="' . $imgUrl . '" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" />', $content);
|
$this->assertStringContainsString('<img src="' . $imgUrl . '" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" />', $content);
|
||||||
|
|
||||||
$formUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
$formUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
||||||
@@ -525,14 +525,14 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testIsTwoFactorImageSecure(): void
|
public function testIsTwoFactorImageSecure(): void
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTwoFactorImageFailsOnMissingSecret(): void
|
public function testTwoFactorImageFailsOnMissingSecret(): void
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
|
||||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||||
$this->assertRouteNotFound($client);
|
$this->assertRouteNotFound($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +546,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
// this is required, so the totp secret is stored in the user entity
|
// this is required, so the totp secret is stored in the user entity
|
||||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
||||||
|
|
||||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||||
self::assertTrue($client->getResponse()->isSuccessful());
|
self::assertTrue($client->getResponse()->isSuccessful());
|
||||||
self::assertEquals('image/png', $client->getResponse()->headers->get('Content-Type'));
|
self::assertEquals('image/png', $client->getResponse()->headers->get('Content-Type'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
|
|||||||
$this->assertStringContainsString('<input type="email"', $content);
|
$this->assertStringContainsString('<input type="email"', $content);
|
||||||
$this->assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content);
|
$this->assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content);
|
||||||
$this->assertStringContainsString('<input type="text"', $content);
|
$this->assertStringContainsString('<input type="text"', $content);
|
||||||
$this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern=".{2,}"', $content);
|
$this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern="', $content);
|
||||||
$this->assertStringContainsString('<input type="password"', $content);
|
$this->assertStringContainsString('<input type="password"', $content);
|
||||||
$this->assertStringContainsString('id="user_registration_form_plainPassword_first" name="user_registration_form[plainPassword][first]" required="required"', $content);
|
$this->assertStringContainsString('id="user_registration_form_plainPassword_first" name="user_registration_form[plainPassword][first]" required="required"', $content);
|
||||||
$this->assertStringContainsString('id="user_registration_form_plainPassword_second" name="user_registration_form[plainPassword][second]" required="required"', $content);
|
$this->assertStringContainsString('id="user_registration_form_plainPassword_second" name="user_registration_form[plainPassword][second]" required="required"', $content);
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ class ExportServiceCompilerPassTest extends TestCase
|
|||||||
private function getContainer(): ContainerBuilder
|
private function getContainer(): ContainerBuilder
|
||||||
{
|
{
|
||||||
$container = new ContainerBuilder();
|
$container = new ContainerBuilder();
|
||||||
$container->setParameter('kimai.export.documents', []); // TODO we could test that as well
|
$container->setParameter('kimai.export.documents', [
|
||||||
|
'templates/export/renderer/',
|
||||||
|
]);
|
||||||
|
|
||||||
$definition = new Definition(ServiceExport::class);
|
$definition = new Definition(ServiceExport::class);
|
||||||
$container->setDefinition(ServiceExport::class, $definition);
|
$container->setDefinition(ServiceExport::class, $definition);
|
||||||
@@ -61,7 +63,8 @@ class ExportServiceCompilerPassTest extends TestCase
|
|||||||
$definition = $container->findDefinition(ServiceExport::class);
|
$definition = $container->findDefinition(ServiceExport::class);
|
||||||
$methods = $definition->getMethodCalls();
|
$methods = $definition->getMethodCalls();
|
||||||
|
|
||||||
self::assertCount(5, $methods);
|
self::assertCount(6, $methods);
|
||||||
|
self::assertTrue($definition->hasMethodCall('addDirectory'));
|
||||||
self::assertTrue($definition->hasMethodCall('addRenderer'));
|
self::assertTrue($definition->hasMethodCall('addRenderer'));
|
||||||
self::assertTrue($definition->hasMethodCall('addTimesheetExporter'));
|
self::assertTrue($definition->hasMethodCall('addTimesheetExporter'));
|
||||||
self::assertTrue($definition->hasMethodCall('addExportRepository'));
|
self::assertTrue($definition->hasMethodCall('addExportRepository'));
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use App\Export\ServiceExport;
|
|||||||
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
|
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\ExportQuery;
|
use App\Repository\Query\ExportQuery;
|
||||||
|
use App\Tests\Mocks\Export\HtmlRendererFactoryMock;
|
||||||
|
use App\Tests\Mocks\Export\PdfRendererFactoryMock;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
@@ -28,9 +30,11 @@ class ServiceExportTest extends TestCase
|
|||||||
{
|
{
|
||||||
private function createSut(): ServiceExport
|
private function createSut(): ServiceExport
|
||||||
{
|
{
|
||||||
$sut = new ServiceExport($this->createMock(EventDispatcherInterface::class));
|
return new ServiceExport(
|
||||||
|
$this->createMock(EventDispatcherInterface::class),
|
||||||
return $sut;
|
(new HtmlRendererFactoryMock($this))->create(),
|
||||||
|
(new PdfRendererFactoryMock($this))->create(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEmptyObject()
|
public function testEmptyObject()
|
||||||
|
|||||||
30
tests/Mocks/Export/HtmlRendererFactoryMock.php
Normal file
30
tests/Mocks/Export/HtmlRendererFactoryMock.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?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\Tests\Mocks\Export;
|
||||||
|
|
||||||
|
use App\Activity\ActivityStatisticService;
|
||||||
|
use App\Export\Renderer\HtmlRendererFactory;
|
||||||
|
use App\Project\ProjectStatisticService;
|
||||||
|
use App\Tests\Mocks\AbstractMockFactory;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class HtmlRendererFactoryMock extends AbstractMockFactory
|
||||||
|
{
|
||||||
|
public function create(): HtmlRendererFactory
|
||||||
|
{
|
||||||
|
return new HtmlRendererFactory(
|
||||||
|
$this->createMock(Environment::class),
|
||||||
|
$this->createMock(EventDispatcherInterface::class),
|
||||||
|
$this->createMock(ProjectStatisticService::class),
|
||||||
|
$this->createMock(ActivityStatisticService::class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tests/Mocks/Export/PdfRendererFactoryMock.php
Normal file
28
tests/Mocks/Export/PdfRendererFactoryMock.php
Normal 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\Tests\Mocks\Export;
|
||||||
|
|
||||||
|
use App\Export\Renderer\PdfRendererFactory;
|
||||||
|
use App\Pdf\HtmlToPdfConverter;
|
||||||
|
use App\Project\ProjectStatisticService;
|
||||||
|
use App\Tests\Mocks\AbstractMockFactory;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class PdfRendererFactoryMock extends AbstractMockFactory
|
||||||
|
{
|
||||||
|
public function create(): PdfRendererFactory
|
||||||
|
{
|
||||||
|
return new PdfRendererFactory(
|
||||||
|
$this->createMock(Environment::class),
|
||||||
|
$this->createMock(HtmlToPdfConverter::class),
|
||||||
|
$this->createMock(ProjectStatisticService::class),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user