Release 2.37 (#5546)
This commit is contained in:
@@ -43,7 +43,10 @@ export default class KimaiDateNowForm extends KimaiFormPlugin {
|
||||
const formElement = document.getElementById(linkTarget.dataset.target);
|
||||
if (!formElement.disabled) {
|
||||
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
|
||||
// this should usually work
|
||||
formElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
// this is required for Litepicker to pick the new date (with autoRefresh option)
|
||||
formElement.dispatchEvent(new Event('keyup', {bubbles: true}));
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -82,6 +82,8 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
||||
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
|
||||
maxOptions: 500,
|
||||
sortField:[{field: '$order'}, {field: '$score'}],
|
||||
// required so it works in table.responsive, but requires z-index 1056, because bootstrap modal would otherwise hide it
|
||||
dropdownParent: 'body',
|
||||
};
|
||||
|
||||
let render = {
|
||||
|
||||
@@ -16,23 +16,151 @@ section.quick-entry-page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* shrinks the dropdown elements for project and activity to fit into the table, instead of adjusting to the longest option element */
|
||||
.ts-wrapper {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
min-height: 36px;
|
||||
max-height: 36px;
|
||||
max-width: 150px;
|
||||
|
||||
.ts-control {
|
||||
white-space: nowrap;
|
||||
.item {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table tbody td:not(.total) {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.duration-widget input.duration-input {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
/* make the duration field and dropdown smaller */
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
min-width: 85px;
|
||||
max-width: 50px;
|
||||
min-width: 50px;
|
||||
}
|
||||
.btn-duration-preset {
|
||||
padding: 7px 8px 7px 5px;
|
||||
display: none;
|
||||
padding: 7px 5px 7px 2px;
|
||||
}
|
||||
input {
|
||||
padding: 7px 3px 7px 7px;
|
||||
input.duration-input {
|
||||
padding: 7px 5px;
|
||||
max-width: 100%;
|
||||
border-bottom-right-radius: var(--tblr-border-radius);
|
||||
border-top-right-radius: var(--tblr-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
table.dataTable thead th,
|
||||
table.dataTable td.total {
|
||||
padding: .75rem 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* make sure dropdown never hides behind table responsive */
|
||||
.dropdown-menu {
|
||||
z-index: 1056;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 210px;
|
||||
}
|
||||
.duration-widget {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
section.quick-entry-page {
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
max-width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
.btn-duration-preset {
|
||||
display: flex;
|
||||
}
|
||||
input.duration-input {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable td.total {
|
||||
padding: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1300px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 230px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 370px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
section.quick-entry-page {
|
||||
.ts-wrapper {
|
||||
max-width: 450px;
|
||||
}
|
||||
#quick_entry_box {
|
||||
.duration-widget {
|
||||
.input-group {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
|
||||
/* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
|
||||
/*
|
||||
1. bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that: use 1021
|
||||
2. bootstrap modal uses 1055, tomselect with dropdownParent: 'body' (required for use in table.responsive, e.g quick.entry on small devices) needs 1056, so it works in modals
|
||||
*/
|
||||
.ts-dropdown {
|
||||
z-index: 1021;
|
||||
z-index: 1056;
|
||||
|
||||
}
|
||||
|
||||
/* Tabler defines a min-width of 7rem, what makes many input fields become a two-line field
|
||||
|
||||
466
composer.lock
generated
466
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ tabler:
|
||||
duration: far fa-hourglass
|
||||
edit: far fa-edit
|
||||
end: fas fa-stopwatch
|
||||
export: fas fa-file-export
|
||||
export: fas fa-download
|
||||
failure: fas fa-times
|
||||
fax: fas fa-fax
|
||||
filter: fas fa-filter
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
public/build/app.3d366594.js
Normal file
2
public/build/app.3d366594.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
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.d4ea12cd.js"
|
||||
"/build/app.3d366594.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.6e7b2701.css"
|
||||
"/build/app.7ffe3b24.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"/build/app-rtl.7a875ca7.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.bfe8d111.css"
|
||||
"/build/app-rtl.4f157a57.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.d4ea12cd.js": "sha384-vv5bzRvK0YN29MXeYe0uKv/LdRlwzMBu+RMvBXpNGu6btx1WxFmDOF6QM+CDoDPB",
|
||||
"/build/app.6e7b2701.css": "sha384-rrrlE5ISpDF53P81i2ce7AKONdm+b+I3mFacFPiJUohuMbMuHnUINac7YDkKD0/p",
|
||||
"/build/app.3d366594.js": "sha384-Wa5ZJDt7hVmlqWA8ixxZ/w/9vh/NJQ74KK3FqAsT/PPWNayIcg0N5XOv/9GfspBk",
|
||||
"/build/app.7ffe3b24.css": "sha384-G4p41N3MjNkWRsaJkUKgGj1Ukd18qMGXHPOaDH6DqE8AzHAJPa1sGskswGP5zg+N",
|
||||
"/build/app-rtl.7a875ca7.js": "sha384-T7gLI61h9dGeMgzo63vKu4GiDOeLPct9zSUHrceNbhSwIdUmSSNoZ1+d7fKhJJ4/",
|
||||
"/build/app-rtl.bfe8d111.css": "sha384-Ty9pGDTW2pxsBS7vfmjpTNFTJS/9jM5V4QfVgLXjIXuuJo5vi8x4sKKBTlwnZuqo",
|
||||
"/build/app-rtl.4f157a57.css": "sha384-kV+s8JUXnXpA6O/j8m7QCvqA5Qb7BK4kk3l1Ojs84OIrg4ztcvf3h6cBb+g/dMfw",
|
||||
"/build/export-pdf.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.773af9c4.js": "sha384-QmMqYJ0RP2WOYU7D4lLzKCBFDL6vCvZHQc7wf8K8N+hdCuTJwq1P0GuY81f30/SY",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.6e7b2701.css",
|
||||
"build/app.js": "/build/app.d4ea12cd.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.bfe8d111.css",
|
||||
"build/app.css": "/build/app.7ffe3b24.css",
|
||||
"build/app.js": "/build/app.3d366594.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.4f157a57.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.7a875ca7.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||
|
||||
@@ -20,19 +20,16 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
abstract class AbstractRoleCommand extends Command
|
||||
{
|
||||
public function __construct(private UserService $userService)
|
||||
public function __construct(private readonly UserService $userService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
new InputArgument('role', InputArgument::OPTIONAL, 'The role'),
|
||||
new InputOption('super', null, InputOption::VALUE_NONE, 'Instead specifying role, use this to quickly add the super administrator role'),
|
||||
]);
|
||||
$this->addArgument('username', InputArgument::REQUIRED, 'The username');
|
||||
$this->addArgument('role', InputArgument::OPTIONAL, 'The role');
|
||||
$this->addOption('super', null, InputOption::VALUE_NONE, 'Instead specifying role, use this to quickly add the super administrator role');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
@@ -26,7 +26,7 @@ abstract class AbstractUserCommand extends Command
|
||||
|
||||
$passwordQuestion = new Question('Please enter the password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
$passwordQuestion->setHiddenFallback(true);
|
||||
$passwordQuestion->setValidator(function (?string $value) {
|
||||
$password = trim($value);
|
||||
if (empty($password)) {
|
||||
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:activate')]
|
||||
#[AsCommand(name: 'kimai:user:activate', description: 'Activate a user')]
|
||||
final class ActivateUserCommand extends Command
|
||||
{
|
||||
public function __construct(private UserService $userService)
|
||||
@@ -28,10 +28,7 @@ final class ActivateUserCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Activate a user')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
])
|
||||
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:activate</info> command activates a user (so they will be able to log in):
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:password')]
|
||||
#[AsCommand(name: 'kimai:user:password', description: 'Change the password of a user')]
|
||||
final class ChangePasswordCommand extends AbstractUserCommand
|
||||
{
|
||||
public function __construct(private UserService $userService)
|
||||
@@ -29,11 +29,8 @@ final class ChangePasswordCommand extends AbstractUserCommand
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Change the password of a user.')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
new InputArgument('password', InputArgument::OPTIONAL, 'The password'),
|
||||
])
|
||||
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||
->addArgument('password', InputArgument::OPTIONAL, 'The password')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:password</info> command changes the password of a user:
|
||||
|
||||
@@ -20,7 +20,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:create')]
|
||||
#[AsCommand(name: 'kimai:user:create', description: 'Create a new user')]
|
||||
final class CreateUserCommand extends AbstractUserCommand
|
||||
{
|
||||
public function __construct(private UserService $userService)
|
||||
@@ -33,7 +33,6 @@ final class CreateUserCommand extends AbstractUserCommand
|
||||
$roles = implode(',', [User::DEFAULT_ROLE, User::ROLE_ADMIN]);
|
||||
|
||||
$this
|
||||
->setDescription('Create a new user')
|
||||
->setHelp('This command allows you to create a new user.')
|
||||
->addArgument('username', InputArgument::REQUIRED, 'A name for the new user (must be unique)')
|
||||
->addArgument('email', InputArgument::REQUIRED, 'Email address of the new user (must be unique)')
|
||||
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:deactivate')]
|
||||
#[AsCommand(name: 'kimai:user:deactivate', description: 'Deactivate a user')]
|
||||
final class DeactivateUserCommand extends Command
|
||||
{
|
||||
public function __construct(private UserService $userService)
|
||||
@@ -28,10 +28,7 @@ final class DeactivateUserCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Deactivate a user')
|
||||
->setDefinition([
|
||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
||||
])
|
||||
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:deactivate</info> command deactivates a user (will not be able to log in)
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\User\UserService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:demote')]
|
||||
#[AsCommand(name: 'kimai:user:demote', description: 'Demote a user by removing a role')]
|
||||
final class DemoteUserCommand extends AbstractRoleCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
@@ -22,7 +22,6 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setDescription('Demote a user by removing a role')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:demote</info> command demotes a user by removing a role
|
||||
|
||||
@@ -31,7 +31,7 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand(name: 'kimai:export:create')]
|
||||
#[AsCommand(name: 'kimai:export:create', description: 'Create exports')]
|
||||
final class ExportCreateCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
@@ -49,7 +49,6 @@ final class ExportCreateCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Create exports')
|
||||
->setHelp('Create exports by several different filters and sent them via email.')
|
||||
->addOption('username', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the export (e.g. used for permissions and decimal setting)')
|
||||
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
||||
|
||||
@@ -34,7 +34,7 @@ use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
#[AsCommand(name: 'kimai:invoice:create')]
|
||||
#[AsCommand(name: 'kimai:invoice:create', description: 'Create invoices')]
|
||||
final class InvoiceCreateCommand extends Command
|
||||
{
|
||||
private ?string $previewDirectory = null;
|
||||
@@ -54,7 +54,6 @@ final class InvoiceCreateCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Create invoices')
|
||||
->setHelp('This command allows to create invoices by several different filters.')
|
||||
->addOption('user', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the invoices')
|
||||
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
||||
|
||||
@@ -16,7 +16,6 @@ use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
@@ -31,7 +30,6 @@ final class MailTestCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('to', InputArgument::REQUIRED, 'The email address to send the email to');
|
||||
$this->addOption('from', null, InputOption::VALUE_OPTIONAL, 'Deprecated: uses the MAILER_FROM env variable.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -41,10 +39,6 @@ final class MailTestCommand extends Command
|
||||
throw new \InvalidArgumentException('Need a non-empty "to" address');
|
||||
}
|
||||
|
||||
if ($input->getOption('from') !== null) {
|
||||
throw new \InvalidArgumentException('The "from" option is deprecated and will be ignored');
|
||||
}
|
||||
|
||||
$message = new Email();
|
||||
$message->to($to);
|
||||
$message->subject('Test email - ' . Constants::SOFTWARE);
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\User\UserService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:promote')]
|
||||
#[AsCommand(name: 'kimai:user:promote', description: 'Promotes a user by adding a role')]
|
||||
final class PromoteUserCommand extends AbstractRoleCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
@@ -22,7 +22,6 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setDescription('Promotes a user by adding a role')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>kimai:user:promote</info> command promotes a user by adding a role
|
||||
|
||||
@@ -19,19 +19,14 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
#[AsCommand(name: 'kimai:timesheet:stop-all')]
|
||||
#[AsCommand(name: 'kimai:timesheet:stop-all', description: 'Stop all running timesheets immediately')]
|
||||
final class TimesheetStopAllCommand extends Command
|
||||
{
|
||||
public function __construct(private TimesheetService $timesheetService)
|
||||
public function __construct(private readonly TimesheetService $timesheetService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Stop all running timesheets immediately');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$amount = $this->timesheetService->stopAll();
|
||||
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\HttpClient\HttpClient;
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
#[AsCommand(name: 'kimai:translations')]
|
||||
#[AsCommand(name: 'kimai:translations', description: 'Automated translation adjustments')]
|
||||
final class TranslationCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
@@ -40,7 +40,6 @@ final class TranslationCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Translation adjustments')
|
||||
->addOption('resname', null, InputOption::VALUE_NONE, 'Fix the resname vs. id attribute')
|
||||
->addOption('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys')
|
||||
->addOption('delete-resname', null, InputOption::VALUE_REQUIRED, 'Deletes the translation by resname')
|
||||
|
||||
@@ -17,17 +17,14 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'kimai:version')]
|
||||
#[AsCommand(name: 'kimai:version', description: 'Show version information')]
|
||||
final class VersionCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Receive version information')
|
||||
->setHelp('This command allows you to fetch various version information about Kimai.')
|
||||
->addOption('short', null, InputOption::VALUE_NONE, 'Display the version only')
|
||||
->addOption('number', null, InputOption::VALUE_NONE, 'Display the version identifier only only')
|
||||
;
|
||||
$this->setHelp('This command allows you to fetch various version information about Kimai.');
|
||||
$this->addOption('short', null, InputOption::VALUE_NONE, 'Display the version only');
|
||||
$this->addOption('number', null, InputOption::VALUE_NONE, 'Display the version identifier only only');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
|
||||
@@ -458,6 +458,11 @@ final class SystemConfiguration
|
||||
return (bool) $this->find('timesheet.rules.break_time_active');
|
||||
}
|
||||
|
||||
public function getExportTimeout(): int
|
||||
{
|
||||
return (int) $this->find('export.timeout');
|
||||
}
|
||||
|
||||
// ========== Company configurations ==========
|
||||
|
||||
public function getFinancialYearStart(): ?string
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.36.1';
|
||||
public const VERSION = '2.37.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 23601;
|
||||
public const VERSION_ID = 23700;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Configuration\SamlConfigurationInterface;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
@@ -76,7 +77,7 @@ final class SamlController extends AbstractController
|
||||
throw new \RuntimeException('SAML login failed');
|
||||
}
|
||||
|
||||
return $this->redirect($url);
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
#[Route(path: '/metadata', name: 'saml_metadata')]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Entity\ExportTemplate;
|
||||
use App\Export\Base\DispositionInlineInterface;
|
||||
@@ -114,7 +115,7 @@ final class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/data', name: 'export_data', methods: ['POST'])]
|
||||
public function export(Request $request): Response
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
@@ -132,6 +133,9 @@ final class ExportController extends AbstractController
|
||||
throw $this->createNotFoundException('Unknown export renderer');
|
||||
}
|
||||
|
||||
$oldMaxExecTime = \ini_get('max_execution_time');
|
||||
ini_set('max_execution_time', $systemConfiguration->getExportTimeout());
|
||||
|
||||
// display file inline if supported and `markAsExported` is not set
|
||||
if ($renderer instanceof DispositionInlineInterface && !$query->isMarkAsExported()) {
|
||||
$renderer->setDispositionInline(true);
|
||||
@@ -144,6 +148,8 @@ final class ExportController extends AbstractController
|
||||
$this->export->setExported($entries);
|
||||
}
|
||||
|
||||
ini_set('max_execution_time', $oldMaxExecTime);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,11 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
abstract class TimesheetAbstractController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
protected TimesheetRepository $repository,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected TimesheetService $service,
|
||||
protected SystemConfiguration $configuration,
|
||||
protected TagRepository $tagRepository
|
||||
protected readonly TimesheetRepository $repository,
|
||||
protected readonly EventDispatcherInterface $dispatcher,
|
||||
protected readonly TimesheetService $service,
|
||||
protected readonly SystemConfiguration $configuration,
|
||||
protected readonly TagRepository $tagRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -263,7 +263,14 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
$entries = $this->repository->getTimesheetResult($query);
|
||||
|
||||
return $exporter->render($entries->getResults(), $query);
|
||||
$oldMaxExecTime = \ini_get('max_execution_time');
|
||||
ini_set('max_execution_time', $this->configuration->getExportTimeout());
|
||||
|
||||
$response = $exporter->render($entries->getResults(), $query);
|
||||
|
||||
ini_set('max_execution_time', $oldMaxExecTime);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function multiUpdate(Request $request): Response
|
||||
|
||||
@@ -396,6 +396,10 @@ final class Configuration implements ConfigurationInterface
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->integerNode('timeout')
|
||||
->defaultValue(60)
|
||||
->end()
|
||||
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class ExportTemplate
|
||||
#[Assert\NotNull]
|
||||
private array $columns = [];
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
* @var array<string, int|string|null|bool>
|
||||
*/
|
||||
#[ORM\Column(name: 'options', type: Types::JSON, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
@@ -106,8 +106,22 @@ class ExportTemplate
|
||||
$this->columns = $columns ?? [];
|
||||
}
|
||||
|
||||
public function getOption(string $key, int|string|bool $default): int|string|bool
|
||||
{
|
||||
if (\array_key_exists($key, $this->options)) {
|
||||
return $this->options[$key] ?? $default;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function setOption(string $key, int|string|null|bool $value): void
|
||||
{
|
||||
$this->options[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @return array<string, int|string|null|bool>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
@@ -115,13 +129,30 @@ class ExportTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @param array<string, int|string|null|bool> $options
|
||||
*/
|
||||
public function setOptions(?array $options): void
|
||||
{
|
||||
$this->options = $options ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used for CSV export
|
||||
*/
|
||||
public function setSeparator(string $separator): void
|
||||
{
|
||||
if (!\in_array($separator, [',', ';'], true)) {
|
||||
throw new \InvalidArgumentException('Invalid separator, comma and semicolon are allowed.');
|
||||
}
|
||||
|
||||
$this->setOption('separator', $separator);
|
||||
}
|
||||
|
||||
public function getSeparator(): string
|
||||
{
|
||||
return (string) $this->getOption('separator', ',');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->title ?? 'New';
|
||||
|
||||
@@ -914,6 +914,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for setUserIdentifier()
|
||||
*
|
||||
* The visible username is setAlias())
|
||||
*/
|
||||
public function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Entity\Invoice;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to invoices
|
||||
* This event can be used, to dynamically add meta-fields to invoices
|
||||
*/
|
||||
final class InvoiceMetaDefinitionEvent extends Event
|
||||
{
|
||||
|
||||
@@ -87,7 +87,12 @@ final class CsvRenderer implements RendererInterface, TimesheetExportInterface
|
||||
$options = new Options();
|
||||
$options->SHOULD_ADD_BOM = false;
|
||||
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer($options), $this->translator, $this->locale);
|
||||
$opts = $this->spreadsheetRenderer->getTemplate()->getOptions();
|
||||
if (\array_key_exists('separator', $opts) && $opts['separator'] === ';') {
|
||||
$options->FIELD_DELIMITER = ';';
|
||||
}
|
||||
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer($options), $this->translator, $this->locale ?? $this->spreadsheetRenderer->getTemplate()->getLocale());
|
||||
$spreadsheet->open($filename);
|
||||
|
||||
$this->spreadsheetRenderer->registerFormatter('date', new DateStringFormatter());
|
||||
|
||||
@@ -65,13 +65,14 @@ final class SpreadsheetRenderer
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
private function getTemplate(): TemplateInterface
|
||||
public function getTemplate(): TemplateInterface
|
||||
{
|
||||
if ($this->template === null) {
|
||||
$template = new Template('default', 'default');
|
||||
$template->setColumns($this->getDefaultColumns());
|
||||
$template->setLocale('en');
|
||||
|
||||
return $template;
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
return $this->template;
|
||||
@@ -263,6 +264,8 @@ final class SpreadsheetRenderer
|
||||
$columns[] = (new Column('alias', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
} elseif ($column === 'user.name') {
|
||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getUserIdentifier())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
} elseif ($column === 'user.email') {
|
||||
$columns[] = (new Column('email', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getEmail())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
} elseif ($column === 'user.account_number') {
|
||||
$columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber());
|
||||
} elseif ($column === 'customer.name') {
|
||||
@@ -336,6 +339,7 @@ final class SpreadsheetRenderer
|
||||
'fixed_rate',
|
||||
'user.alias',
|
||||
'user.name',
|
||||
'user.email',
|
||||
'user.account_number',
|
||||
'customer.name',
|
||||
'project.name',
|
||||
|
||||
@@ -81,7 +81,7 @@ final class XlsxRenderer implements RendererInterface, TimesheetExportInterface
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer(), $this->translator, $this->locale);
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer(), $this->translator, $this->locale ?? $this->spreadsheetRenderer->getTemplate()->getLocale());
|
||||
$spreadsheet->open($filename);
|
||||
|
||||
$this->spreadsheetRenderer->writeSpreadsheet($spreadsheet, $exportItems, $query);
|
||||
|
||||
@@ -85,6 +85,7 @@ final class ServiceExport
|
||||
$tpl = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||
$tpl->setColumns($template->getColumns());
|
||||
$tpl->setLocale($template->getLanguage());
|
||||
$tpl->setOptions($template->getOptions());
|
||||
|
||||
switch ($template->getRenderer()) {
|
||||
case 'csv':
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Form\Type\ExportColumnsType;
|
||||
use App\Form\Type\ExportRendererType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -26,6 +27,7 @@ class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
$builder->add('renderer', ExportRendererType::class, ['label' => 'type']);
|
||||
$builder->add('language', LanguageType::class, ['required' => false]);
|
||||
$builder->add('columns', ExportColumnsType::class, ['required' => true]);
|
||||
$builder->add('separator', ChoiceType::class, ['choices' => ['Comma (,)' => ',', 'Semicolon (;)' => ';'], 'required' => true]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -61,7 +61,7 @@ final class QuickEntryTimesheetType extends AbstractType
|
||||
}
|
||||
|
||||
if ($data instanceof Timesheet && !$this->security->isGranted('edit', $data)) {
|
||||
$event->getForm()->remove('duration');
|
||||
// do not call $event->getForm()->remove() this would change the field order
|
||||
$event->getForm()->add('duration', DurationType::class, array_merge(['disabled' => true], $durationOptions));
|
||||
|
||||
$mainForm = $event->getForm()->getParent()?->getParent();
|
||||
@@ -79,7 +79,7 @@ final class QuickEntryTimesheetType extends AbstractType
|
||||
continue;
|
||||
}
|
||||
$type = \get_class($child->getConfig()->getType()->getInnerType());
|
||||
$mainForm->remove($key);
|
||||
// do not call $mainForm->remove() this would change the field order
|
||||
$mainForm->add($key, $type, array_merge($child->getConfig()->getOptions(), ['disabled' => true]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
{% if btnTitle == id %}
|
||||
{% set btnTitle = title|trans %}
|
||||
{% endif %}
|
||||
{% if btnTitle == id%}
|
||||
{% if btnTitle == id %}
|
||||
{% set btnTitle = id|split('.')|first|replace({'-': ' ', '_': ' '})|split(' ')|map(t => t|capitalize)|join(' ') %}
|
||||
{% endif %}
|
||||
<a href="#" class="dropdown-item startExportBtn" data-type="{{ id }}">
|
||||
|
||||
@@ -14,11 +14,30 @@
|
||||
{{ form_row(form.title) }}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form_row(form.renderer) }}
|
||||
{{ form_row(form.renderer, {attr: {onchange: "changeExportTemplateType(this)"}}) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.separator, {row_attr: {class: 'mb-3 export-type csv'}}) }}
|
||||
{{ form_row(form.columns) }}
|
||||
{{ form_rest(form) }}
|
||||
{% endblock %}
|
||||
{% block form_after %}
|
||||
<script>
|
||||
function changeExportTemplateType(typeElement)
|
||||
{
|
||||
typeElement.form.querySelectorAll('.export-type').forEach(
|
||||
element => {
|
||||
if (element.classList.contains(typeElement.value)) {
|
||||
element.classList.remove('d-none');
|
||||
} else {
|
||||
element.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
changeExportTemplateType(document.getElementById('{{ form.renderer.vars.id }}'));
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
{% endif %}
|
||||
{% elseif 'DurationType' in type %}
|
||||
{{ value|duration }}
|
||||
{% elseif 'YesNoType' in type or 'CheckBoxType' in type %}
|
||||
{% elseif 'YesNoType' in type or 'CheckboxType' in type %}
|
||||
{{ _self.label_boolean(value) }}
|
||||
{% elseif 'DatePickerType' in type %}
|
||||
{{ value|date_short }}
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
<td></td>
|
||||
{% endfor %}
|
||||
{% for id, week in days %}
|
||||
<td class="text-center" id="qe-totals-day-{{ loop.index0 }}"></td>
|
||||
<td class="text-center total" id="qe-totals-day-{{ loop.index0 }}"></td>
|
||||
{% endfor %}
|
||||
<td class="text-center" id="qe-totals-week"></td>
|
||||
<td class="text-center total" id="qe-totals-week"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -66,25 +66,15 @@
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
document.addEventListener('kimai.initialized', function(event) {
|
||||
const kimai = event.detail.kimai;
|
||||
const DATES = kimai.getPlugin('date');
|
||||
/** @type {KimaiFormSelect} FORM_SELECT */
|
||||
const FORM_SELECT = kimai.getPlugin('form-select');
|
||||
|
||||
if (window.matchMedia("(max-width: 1000px)").matches) {
|
||||
for (let dropdowns of document.querySelectorAll('tr.qe-entry-week-row .btn-duration-preset')) {
|
||||
dropdowns.style.display = 'none';
|
||||
}
|
||||
|
||||
for (let tmp of document.querySelectorAll('tr.qe-entry-week-row .duration-widget .input-group')) {
|
||||
tmp.style.maxWidth = '50px';
|
||||
tmp.style.minWidth = '50px';
|
||||
}
|
||||
for (let input of document.querySelectorAll('tr.qe-entry-week-row input.duration-input')) {
|
||||
input.style.maxWidth = '50px';
|
||||
}
|
||||
if (window.matchMedia("(max-width: 1360px)").matches) {
|
||||
document.querySelector('form[name=quick_entry_form] div.maybe-table-responsive').classList.replace('maybe-table-responsive', 'table-responsive');
|
||||
}
|
||||
|
||||
@@ -139,7 +129,6 @@
|
||||
|
||||
collectionHolder.appendChild(node);
|
||||
|
||||
|
||||
[].slice.call(node.querySelectorAll('.selectpicker')).map((element) => {
|
||||
FORM_SELECT.activateSelectPickerByElement(element);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="card-footer">
|
||||
{% if user.totpAuthenticationEnabled %}
|
||||
{{ form_start(deactivate, {'attr': {'id': 'user_two_factor_form'}}) }}
|
||||
<input type="submit" value="{{ 'deactivate'|trans }}" class="btn btn-warning" id="user_two_factor_button" />
|
||||
<input type="submit" value="{{ 'deactivate'|trans }}" class="btn btn-warning confirmation-link" id="user_two_factor_button" data-question="{{ 'deactivate'|trans }}?" />
|
||||
{{ form_end(deactivate) }}
|
||||
{% else %}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" id="user_two_factor_button" />
|
||||
|
||||
40
tests/Command/MailTestCommandTest.php
Normal file
40
tests/Command/MailTestCommandTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\Command\MailTestCommand;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\MailTestCommand
|
||||
* @group integration
|
||||
*/
|
||||
class MailTestCommandTest extends KernelTestCase
|
||||
{
|
||||
protected Application $application;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$this->application->add(new MailTestCommand(
|
||||
$this->createMock(EventDispatcherInterface::class)
|
||||
));
|
||||
}
|
||||
|
||||
public function testCommandName(): void
|
||||
{
|
||||
$command = $this->application->find('kimai:mail:test');
|
||||
self::assertInstanceOf(MailTestCommand::class, $command);
|
||||
}
|
||||
}
|
||||
@@ -319,6 +319,7 @@ class ConfigurationTest extends TestCase
|
||||
0 => 'var/export/',
|
||||
1 => 'templates/export/renderer/',
|
||||
],
|
||||
'timeout' => 60,
|
||||
],
|
||||
'calendar' => [
|
||||
'week_numbers' => true,
|
||||
|
||||
@@ -54,10 +54,28 @@ class ExportTemplateTest extends AbstractEntityTestCase
|
||||
|
||||
$sut->setOptions(['foo' => 1, 'bar' => true, 'WORLD' => 'HELLO']);
|
||||
self::assertEquals(['foo' => 1, 'bar' => true, 'WORLD' => 'HELLO'], $sut->getOptions());
|
||||
$sut->setOption('foo', 4711);
|
||||
$sut->setOption('empty', null);
|
||||
$sut->setOption('bar', false);
|
||||
$sut->setOption('hello', 'kimai');
|
||||
self::assertEquals(['foo' => 4711, 'bar' => false, 'WORLD' => 'HELLO', 'empty' => null, 'hello' => 'kimai'], $sut->getOptions());
|
||||
$sut->setOptions(null);
|
||||
self::assertEquals([], $sut->getOptions());
|
||||
}
|
||||
|
||||
public function testSeparator(): void
|
||||
{
|
||||
$sut = new ExportTemplate();
|
||||
self::assertEquals(',', $sut->getSeparator());
|
||||
$sut->setSeparator(';');
|
||||
self::assertEquals(';', $sut->getSeparator());
|
||||
$sut->setSeparator(',');
|
||||
self::assertEquals(',', $sut->getSeparator());
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$sut->setSeparator('.');
|
||||
}
|
||||
|
||||
public function testClone(): void
|
||||
{
|
||||
$sut = new ExportTemplate();
|
||||
|
||||
@@ -65,13 +65,13 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
{
|
||||
$en = [
|
||||
'Date', 'From', 'To', 'Duration', 'Currency', 'Price', 'Internal price', 'Hourly price', 'Fixed price', 'Name',
|
||||
'User', 'Staff number', 'Customer', 'Project', 'Activity', 'Description', 'Billable', 'Tags',
|
||||
'User', 'E-mail', 'Staff number', 'Customer', 'Project', 'Activity', 'Description', 'Billable', 'Tags',
|
||||
'Type', 'category', 'Account', 'Project number', 'VAT-ID', 'Order number',
|
||||
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
||||
];
|
||||
$de = [
|
||||
'Datum', 'Von', 'Bis', 'Dauer', 'Währung', 'Preis', 'Interner Preis', 'Preis pro Stunde', 'Festpreis', 'Name',
|
||||
'Benutzer', 'Personalnummer', 'Kunde', 'Projekt', 'Tätigkeit', 'Beschreibung', 'Abrechenbar', 'Schlagworte',
|
||||
'Benutzer', 'E-Mail', 'Personalnummer', 'Kunde', 'Projekt', 'Tätigkeit', 'Beschreibung', 'Abrechenbar', 'Schlagworte',
|
||||
'Typ', 'category', 'Kundennummer', 'Projektnummer', 'Umsatzsteuer-ID', 'Bestellnummer',
|
||||
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
||||
];
|
||||
@@ -140,6 +140,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
'Kevin',
|
||||
'kevin',
|
||||
'',
|
||||
'',
|
||||
'Customer Name',
|
||||
'project name',
|
||||
'activity description',
|
||||
@@ -175,6 +176,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
'niveK',
|
||||
'nivek',
|
||||
'',
|
||||
'',
|
||||
'Customer Name',
|
||||
'project name',
|
||||
'activity description',
|
||||
@@ -200,6 +202,6 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
self::assertEquals($expected, $all[5]);
|
||||
self::assertEquals($expected2, $all[6]);
|
||||
self::assertEquals(\count($expected), \count($all[0]));
|
||||
self::assertEquals('foo', $all[4][17]);
|
||||
self::assertEquals('foo', $all[4][18]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ class SpreadsheetRendererTest extends AbstractRendererTestCase
|
||||
'project.meta.project-foo2' => 'Working place',
|
||||
'activity.meta.activity-foo' => 'Working place',
|
||||
'user.meta.mypref' => 'mypref',
|
||||
'email' => 'email'
|
||||
]];
|
||||
|
||||
$template = new Template('test', 'Testing');
|
||||
|
||||
Reference in New Issue
Block a user