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);
|
const formElement = document.getElementById(linkTarget.dataset.target);
|
||||||
if (!formElement.disabled) {
|
if (!formElement.disabled) {
|
||||||
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
|
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
|
||||||
|
// this should usually work
|
||||||
formElement.dispatchEvent(new Event('change', {bubbles: true}));
|
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();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
|
|||||||
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
|
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
|
||||||
maxOptions: 500,
|
maxOptions: 500,
|
||||||
sortField:[{field: '$order'}, {field: '$score'}],
|
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 = {
|
let render = {
|
||||||
|
|||||||
@@ -16,23 +16,151 @@ section.quick-entry-page {
|
|||||||
display: none;
|
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 {
|
.ts-wrapper {
|
||||||
display: table;
|
min-height: 36px;
|
||||||
table-layout: fixed;
|
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 */
|
/* make the duration field and dropdown smaller */
|
||||||
#quick_entry_box {
|
#quick_entry_box {
|
||||||
.duration-widget {
|
.duration-widget {
|
||||||
.input-group {
|
.input-group {
|
||||||
min-width: 85px;
|
max-width: 50px;
|
||||||
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
.btn-duration-preset {
|
.btn-duration-preset {
|
||||||
padding: 7px 8px 7px 5px;
|
display: none;
|
||||||
|
padding: 7px 5px 7px 2px;
|
||||||
|
}
|
||||||
|
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%;
|
||||||
}
|
}
|
||||||
input {
|
|
||||||
padding: 7px 3px 7px 7px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
.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
|
/* 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
|
duration: far fa-hourglass
|
||||||
edit: far fa-edit
|
edit: far fa-edit
|
||||||
end: fas fa-stopwatch
|
end: fas fa-stopwatch
|
||||||
export: fas fa-file-export
|
export: fas fa-download
|
||||||
failure: fas fa-times
|
failure: fas fa-times
|
||||||
fax: fas fa-fax
|
fax: fas fa-fax
|
||||||
filter: fas fa-filter
|
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": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.6c399d29.js",
|
"/build/runtime.6c399d29.js",
|
||||||
"/build/app.d4ea12cd.js"
|
"/build/app.3d366594.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.6e7b2701.css"
|
"/build/app.7ffe3b24.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"app-rtl": {
|
"app-rtl": {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"/build/app-rtl.7a875ca7.js"
|
"/build/app-rtl.7a875ca7.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app-rtl.bfe8d111.css"
|
"/build/app-rtl.4f157a57.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"export-pdf": {
|
"export-pdf": {
|
||||||
@@ -72,10 +72,10 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||||
"/build/app.d4ea12cd.js": "sha384-vv5bzRvK0YN29MXeYe0uKv/LdRlwzMBu+RMvBXpNGu6btx1WxFmDOF6QM+CDoDPB",
|
"/build/app.3d366594.js": "sha384-Wa5ZJDt7hVmlqWA8ixxZ/w/9vh/NJQ74KK3FqAsT/PPWNayIcg0N5XOv/9GfspBk",
|
||||||
"/build/app.6e7b2701.css": "sha384-rrrlE5ISpDF53P81i2ce7AKONdm+b+I3mFacFPiJUohuMbMuHnUINac7YDkKD0/p",
|
"/build/app.7ffe3b24.css": "sha384-G4p41N3MjNkWRsaJkUKgGj1Ukd18qMGXHPOaDH6DqE8AzHAJPa1sGskswGP5zg+N",
|
||||||
"/build/app-rtl.7a875ca7.js": "sha384-T7gLI61h9dGeMgzo63vKu4GiDOeLPct9zSUHrceNbhSwIdUmSSNoZ1+d7fKhJJ4/",
|
"/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.395749ab.js": "sha384-3Hjvmu4FC/0dhHnR8kyRBU7k2xMNy1lxBpGgOkrw8PxXnwyQDM8/5bQmkJbjVT1+",
|
||||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||||
"/build/invoice.773af9c4.js": "sha384-QmMqYJ0RP2WOYU7D4lLzKCBFDL6vCvZHQc7wf8K8N+hdCuTJwq1P0GuY81f30/SY",
|
"/build/invoice.773af9c4.js": "sha384-QmMqYJ0RP2WOYU7D4lLzKCBFDL6vCvZHQc7wf8K8N+hdCuTJwq1P0GuY81f30/SY",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "/build/app.6e7b2701.css",
|
"build/app.css": "/build/app.7ffe3b24.css",
|
||||||
"build/app.js": "/build/app.d4ea12cd.js",
|
"build/app.js": "/build/app.3d366594.js",
|
||||||
"build/app-rtl.css": "/build/app-rtl.bfe8d111.css",
|
"build/app-rtl.css": "/build/app-rtl.4f157a57.css",
|
||||||
"build/app-rtl.js": "/build/app-rtl.7a875ca7.js",
|
"build/app-rtl.js": "/build/app-rtl.7a875ca7.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.395749ab.js",
|
"build/export-pdf.js": "/build/export-pdf.395749ab.js",
|
||||||
|
|||||||
@@ -20,19 +20,16 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||||||
|
|
||||||
abstract class AbstractRoleCommand extends Command
|
abstract class AbstractRoleCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(private UserService $userService)
|
public function __construct(private readonly UserService $userService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this->addArgument('username', InputArgument::REQUIRED, 'The username');
|
||||||
->setDefinition([
|
$this->addArgument('role', InputArgument::OPTIONAL, 'The role');
|
||||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
$this->addOption('super', null, InputOption::VALUE_NONE, 'Instead specifying role, use this to quickly add the super administrator role');
|
||||||
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'),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
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 = new Question('Please enter the password: ');
|
||||||
$passwordQuestion->setHidden(true);
|
$passwordQuestion->setHidden(true);
|
||||||
$passwordQuestion->setHiddenFallback(false);
|
$passwordQuestion->setHiddenFallback(true);
|
||||||
$passwordQuestion->setValidator(function (?string $value) {
|
$passwordQuestion->setValidator(function (?string $value) {
|
||||||
$password = trim($value);
|
$password = trim($value);
|
||||||
if (empty($password)) {
|
if (empty($password)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class ActivateUserCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(private UserService $userService)
|
public function __construct(private UserService $userService)
|
||||||
@@ -28,10 +28,7 @@ final class ActivateUserCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Activate a user')
|
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||||
->setDefinition([
|
|
||||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
|
||||||
])
|
|
||||||
->setHelp(
|
->setHelp(
|
||||||
<<<'EOT'
|
<<<'EOT'
|
||||||
The <info>kimai:user:activate</info> command activates a user (so they will be able to log in):
|
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\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class ChangePasswordCommand extends AbstractUserCommand
|
||||||
{
|
{
|
||||||
public function __construct(private UserService $userService)
|
public function __construct(private UserService $userService)
|
||||||
@@ -29,11 +29,8 @@ final class ChangePasswordCommand extends AbstractUserCommand
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Change the password of a user.')
|
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||||
->setDefinition([
|
->addArgument('password', InputArgument::OPTIONAL, 'The password')
|
||||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
|
||||||
new InputArgument('password', InputArgument::OPTIONAL, 'The password'),
|
|
||||||
])
|
|
||||||
->setHelp(
|
->setHelp(
|
||||||
<<<'EOT'
|
<<<'EOT'
|
||||||
The <info>kimai:user:password</info> command changes the password of a user:
|
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\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class CreateUserCommand extends AbstractUserCommand
|
||||||
{
|
{
|
||||||
public function __construct(private UserService $userService)
|
public function __construct(private UserService $userService)
|
||||||
@@ -33,7 +33,6 @@ final class CreateUserCommand extends AbstractUserCommand
|
|||||||
$roles = implode(',', [User::DEFAULT_ROLE, User::ROLE_ADMIN]);
|
$roles = implode(',', [User::DEFAULT_ROLE, User::ROLE_ADMIN]);
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->setDescription('Create a new user')
|
|
||||||
->setHelp('This command allows you to 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('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)')
|
->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\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class DeactivateUserCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(private UserService $userService)
|
public function __construct(private UserService $userService)
|
||||||
@@ -28,10 +28,7 @@ final class DeactivateUserCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Deactivate a user')
|
->addArgument('username', InputArgument::REQUIRED, 'The username')
|
||||||
->setDefinition([
|
|
||||||
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
|
|
||||||
])
|
|
||||||
->setHelp(
|
->setHelp(
|
||||||
<<<'EOT'
|
<<<'EOT'
|
||||||
The <info>kimai:user:deactivate</info> command deactivates a user (will not be able to log in)
|
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\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class DemoteUserCommand extends AbstractRoleCommand
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -22,7 +22,6 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
|||||||
parent::configure();
|
parent::configure();
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->setDescription('Demote a user by removing a role')
|
|
||||||
->setHelp(
|
->setHelp(
|
||||||
<<<'EOT'
|
<<<'EOT'
|
||||||
The <info>kimai:user:demote</info> command demotes a user by removing a role
|
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\LocaleAwareInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[AsCommand(name: 'kimai:export:create')]
|
#[AsCommand(name: 'kimai:export:create', description: 'Create exports')]
|
||||||
final class ExportCreateCommand extends Command
|
final class ExportCreateCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -49,7 +49,6 @@ final class ExportCreateCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Create exports')
|
|
||||||
->setHelp('Create exports by several different filters and sent them via email.')
|
->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('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)
|
->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\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
#[AsCommand(name: 'kimai:invoice:create')]
|
#[AsCommand(name: 'kimai:invoice:create', description: 'Create invoices')]
|
||||||
final class InvoiceCreateCommand extends Command
|
final class InvoiceCreateCommand extends Command
|
||||||
{
|
{
|
||||||
private ?string $previewDirectory = null;
|
private ?string $previewDirectory = null;
|
||||||
@@ -54,7 +54,6 @@ final class InvoiceCreateCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Create invoices')
|
|
||||||
->setHelp('This command allows to create invoices by several different filters.')
|
->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('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)
|
->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\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ final class MailTestCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->addArgument('to', InputArgument::REQUIRED, 'The email address to send the email to');
|
$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
|
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');
|
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 = new Email();
|
||||||
$message->to($to);
|
$message->to($to);
|
||||||
$message->subject('Test email - ' . Constants::SOFTWARE);
|
$message->subject('Test email - ' . Constants::SOFTWARE);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use App\User\UserService;
|
|||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
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
|
final class PromoteUserCommand extends AbstractRoleCommand
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -22,7 +22,6 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
|||||||
parent::configure();
|
parent::configure();
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->setDescription('Promotes a user by adding a role')
|
|
||||||
->setHelp(
|
->setHelp(
|
||||||
<<<'EOT'
|
<<<'EOT'
|
||||||
The <info>kimai:user:promote</info> command promotes a user by adding a role
|
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
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
#[AsCommand(name: 'kimai:timesheet:stop-all')]
|
#[AsCommand(name: 'kimai:timesheet:stop-all', description: 'Stop all running timesheets immediately')]
|
||||||
final class TimesheetStopAllCommand extends Command
|
final class TimesheetStopAllCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(private TimesheetService $timesheetService)
|
public function __construct(private readonly TimesheetService $timesheetService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Stop all running timesheets immediately');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$amount = $this->timesheetService->stopAll();
|
$amount = $this->timesheetService->stopAll();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use Symfony\Component\HttpClient\HttpClient;
|
|||||||
*
|
*
|
||||||
* @codeCoverageIgnore
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
#[AsCommand(name: 'kimai:translations')]
|
#[AsCommand(name: 'kimai:translations', description: 'Automated translation adjustments')]
|
||||||
final class TranslationCommand extends Command
|
final class TranslationCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -40,7 +40,6 @@ final class TranslationCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setDescription('Translation adjustments')
|
|
||||||
->addOption('resname', null, InputOption::VALUE_NONE, 'Fix the resname vs. id attribute')
|
->addOption('resname', null, InputOption::VALUE_NONE, 'Fix the resname vs. id attribute')
|
||||||
->addOption('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys')
|
->addOption('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys')
|
||||||
->addOption('delete-resname', null, InputOption::VALUE_REQUIRED, 'Deletes the translation by resname')
|
->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\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
#[AsCommand(name: 'kimai:version')]
|
#[AsCommand(name: 'kimai:version', description: 'Show version information')]
|
||||||
final class VersionCommand extends Command
|
final class VersionCommand extends Command
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this->setHelp('This command allows you to fetch various version information about Kimai.');
|
||||||
->setDescription('Receive version information')
|
$this->addOption('short', null, InputOption::VALUE_NONE, 'Display the version only');
|
||||||
->setHelp('This command allows you to fetch various version information about Kimai.')
|
$this->addOption('number', null, InputOption::VALUE_NONE, 'Display the version identifier only only');
|
||||||
->addOption('short', null, InputOption::VALUE_NONE, 'Display the version only')
|
|
||||||
->addOption('number', null, InputOption::VALUE_NONE, 'Display the version identifier only only')
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
|||||||
@@ -458,6 +458,11 @@ final class SystemConfiguration
|
|||||||
return (bool) $this->find('timesheet.rules.break_time_active');
|
return (bool) $this->find('timesheet.rules.break_time_active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getExportTimeout(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('export.timeout');
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Company configurations ==========
|
// ========== Company configurations ==========
|
||||||
|
|
||||||
public function getFinancialYearStart(): ?string
|
public function getFinancialYearStart(): ?string
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ final class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.36.1';
|
public const VERSION = '2.37.0';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 23601;
|
public const VERSION_ID = 23700;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Configuration\SamlConfigurationInterface;
|
|||||||
use App\Saml\SamlAuthFactory;
|
use App\Saml\SamlAuthFactory;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||||
@@ -76,7 +77,7 @@ final class SamlController extends AbstractController
|
|||||||
throw new \RuntimeException('SAML login failed');
|
throw new \RuntimeException('SAML login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirect($url);
|
return new RedirectResponse($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/metadata', name: 'saml_metadata')]
|
#[Route(path: '/metadata', name: 'saml_metadata')]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\ExportableItem;
|
use App\Entity\ExportableItem;
|
||||||
use App\Entity\ExportTemplate;
|
use App\Entity\ExportTemplate;
|
||||||
use App\Export\Base\DispositionInlineInterface;
|
use App\Export\Base\DispositionInlineInterface;
|
||||||
@@ -114,7 +115,7 @@ final class ExportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/data', name: 'export_data', methods: ['POST'])]
|
#[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();
|
$query = $this->getDefaultQuery();
|
||||||
|
|
||||||
@@ -132,6 +133,9 @@ final class ExportController extends AbstractController
|
|||||||
throw $this->createNotFoundException('Unknown export renderer');
|
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
|
// display file inline if supported and `markAsExported` is not set
|
||||||
if ($renderer instanceof DispositionInlineInterface && !$query->isMarkAsExported()) {
|
if ($renderer instanceof DispositionInlineInterface && !$query->isMarkAsExported()) {
|
||||||
$renderer->setDispositionInline(true);
|
$renderer->setDispositionInline(true);
|
||||||
@@ -144,6 +148,8 @@ final class ExportController extends AbstractController
|
|||||||
$this->export->setExported($entries);
|
$this->export->setExported($entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ini_set('max_execution_time', $oldMaxExecTime);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
abstract class TimesheetAbstractController extends AbstractController
|
abstract class TimesheetAbstractController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected TimesheetRepository $repository,
|
protected readonly TimesheetRepository $repository,
|
||||||
protected EventDispatcherInterface $dispatcher,
|
protected readonly EventDispatcherInterface $dispatcher,
|
||||||
protected TimesheetService $service,
|
protected readonly TimesheetService $service,
|
||||||
protected SystemConfiguration $configuration,
|
protected readonly SystemConfiguration $configuration,
|
||||||
protected TagRepository $tagRepository
|
protected readonly TagRepository $tagRepository
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +263,14 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
|
|
||||||
$entries = $this->repository->getTimesheetResult($query);
|
$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
|
protected function multiUpdate(Request $request): Response
|
||||||
|
|||||||
@@ -396,6 +396,10 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->scalarPrototype()->end()
|
->scalarPrototype()->end()
|
||||||
->defaultValue([])
|
->defaultValue([])
|
||||||
->end()
|
->end()
|
||||||
|
->integerNode('timeout')
|
||||||
|
->defaultValue(60)
|
||||||
|
->end()
|
||||||
|
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class ExportTemplate
|
|||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private array $columns = [];
|
private array $columns = [];
|
||||||
/**
|
/**
|
||||||
* @var array<string, mixed>
|
* @var array<string, int|string|null|bool>
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'options', type: Types::JSON, nullable: false)]
|
#[ORM\Column(name: 'options', type: Types::JSON, nullable: false)]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
@@ -106,8 +106,22 @@ class ExportTemplate
|
|||||||
$this->columns = $columns ?? [];
|
$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
|
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
|
public function setOptions(?array $options): void
|
||||||
{
|
{
|
||||||
$this->options = $options ?? [];
|
$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
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return $this->title ?? 'New';
|
return $this->title ?? 'New';
|
||||||
|
|||||||
@@ -914,6 +914,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for setUserIdentifier()
|
||||||
|
*
|
||||||
|
* The visible username is setAlias())
|
||||||
|
*/
|
||||||
public function setUsername(string $username): void
|
public function setUsername(string $username): void
|
||||||
{
|
{
|
||||||
$this->username = $username;
|
$this->username = $username;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Entity\Invoice;
|
|||||||
use Symfony\Contracts\EventDispatcher\Event;
|
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
|
final class InvoiceMetaDefinitionEvent extends Event
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ final class CsvRenderer implements RendererInterface, TimesheetExportInterface
|
|||||||
$options = new Options();
|
$options = new Options();
|
||||||
$options->SHOULD_ADD_BOM = false;
|
$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);
|
$spreadsheet->open($filename);
|
||||||
|
|
||||||
$this->spreadsheetRenderer->registerFormatter('date', new DateStringFormatter());
|
$this->spreadsheetRenderer->registerFormatter('date', new DateStringFormatter());
|
||||||
|
|||||||
@@ -65,13 +65,14 @@ final class SpreadsheetRenderer
|
|||||||
$this->template = $template;
|
$this->template = $template;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTemplate(): TemplateInterface
|
public function getTemplate(): TemplateInterface
|
||||||
{
|
{
|
||||||
if ($this->template === null) {
|
if ($this->template === null) {
|
||||||
$template = new Template('default', 'default');
|
$template = new Template('default', 'default');
|
||||||
$template->setColumns($this->getDefaultColumns());
|
$template->setColumns($this->getDefaultColumns());
|
||||||
|
$template->setLocale('en');
|
||||||
|
|
||||||
return $template;
|
$this->template = $template;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->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);
|
$columns[] = (new Column('alias', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||||
} elseif ($column === 'user.name') {
|
} elseif ($column === 'user.name') {
|
||||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getUserIdentifier())->withColumnWidth(ColumnWidth::MEDIUM);
|
$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') {
|
} elseif ($column === 'user.account_number') {
|
||||||
$columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber());
|
$columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber());
|
||||||
} elseif ($column === 'customer.name') {
|
} elseif ($column === 'customer.name') {
|
||||||
@@ -336,6 +339,7 @@ final class SpreadsheetRenderer
|
|||||||
'fixed_rate',
|
'fixed_rate',
|
||||||
'user.alias',
|
'user.alias',
|
||||||
'user.name',
|
'user.name',
|
||||||
|
'user.email',
|
||||||
'user.account_number',
|
'user.account_number',
|
||||||
'customer.name',
|
'customer.name',
|
||||||
'project.name',
|
'project.name',
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ final class XlsxRenderer implements RendererInterface, TimesheetExportInterface
|
|||||||
throw new \Exception('Could not open temporary file');
|
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);
|
$spreadsheet->open($filename);
|
||||||
|
|
||||||
$this->spreadsheetRenderer->writeSpreadsheet($spreadsheet, $exportItems, $query);
|
$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 = new Template((string) $template->getId(), $template->getTitle()); // @phpstan-ignore argument.type
|
||||||
$tpl->setColumns($template->getColumns());
|
$tpl->setColumns($template->getColumns());
|
||||||
$tpl->setLocale($template->getLanguage());
|
$tpl->setLocale($template->getLanguage());
|
||||||
|
$tpl->setOptions($template->getOptions());
|
||||||
|
|
||||||
switch ($template->getRenderer()) {
|
switch ($template->getRenderer()) {
|
||||||
case 'csv':
|
case 'csv':
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Form\Type\ExportColumnsType;
|
|||||||
use App\Form\Type\ExportRendererType;
|
use App\Form\Type\ExportRendererType;
|
||||||
use App\Form\Type\LanguageType;
|
use App\Form\Type\LanguageType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
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\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -26,6 +27,7 @@ class ExportTemplateSpreadsheetForm extends AbstractType
|
|||||||
$builder->add('renderer', ExportRendererType::class, ['label' => 'type']);
|
$builder->add('renderer', ExportRendererType::class, ['label' => 'type']);
|
||||||
$builder->add('language', LanguageType::class, ['required' => false]);
|
$builder->add('language', LanguageType::class, ['required' => false]);
|
||||||
$builder->add('columns', ExportColumnsType::class, ['required' => true]);
|
$builder->add('columns', ExportColumnsType::class, ['required' => true]);
|
||||||
|
$builder->add('separator', ChoiceType::class, ['choices' => ['Comma (,)' => ',', 'Semicolon (;)' => ';'], 'required' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ final class QuickEntryTimesheetType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($data instanceof Timesheet && !$this->security->isGranted('edit', $data)) {
|
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));
|
$event->getForm()->add('duration', DurationType::class, array_merge(['disabled' => true], $durationOptions));
|
||||||
|
|
||||||
$mainForm = $event->getForm()->getParent()?->getParent();
|
$mainForm = $event->getForm()->getParent()?->getParent();
|
||||||
@@ -79,7 +79,7 @@ final class QuickEntryTimesheetType extends AbstractType
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$type = \get_class($child->getConfig()->getType()->getInnerType());
|
$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]));
|
$mainForm->add($key, $type, array_merge($child->getConfig()->getOptions(), ['disabled' => true]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
{% if btnTitle == id %}
|
{% if btnTitle == id %}
|
||||||
{% set btnTitle = title|trans %}
|
{% set btnTitle = title|trans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if btnTitle == id%}
|
{% if btnTitle == id %}
|
||||||
{% set btnTitle = id|split('.')|first|replace({'-': ' ', '_': ' '})|split(' ')|map(t => t|capitalize)|join(' ') %}
|
{% set btnTitle = id|split('.')|first|replace({'-': ' ', '_': ' '})|split(' ')|map(t => t|capitalize)|join(' ') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="#" class="dropdown-item startExportBtn" data-type="{{ id }}">
|
<a href="#" class="dropdown-item startExportBtn" data-type="{{ id }}">
|
||||||
|
|||||||
@@ -14,11 +14,30 @@
|
|||||||
{{ form_row(form.title) }}
|
{{ form_row(form.title) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{{ form_row(form.renderer) }}
|
{{ form_row(form.renderer, {attr: {onchange: "changeExportTemplateType(this)"}}) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ form_row(form.language) }}
|
{{ form_row(form.language) }}
|
||||||
|
{{ form_row(form.separator, {row_attr: {class: 'mb-3 export-type csv'}}) }}
|
||||||
|
{{ form_row(form.columns) }}
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{% endblock %}
|
{% 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 %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -338,7 +338,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% elseif 'DurationType' in type %}
|
{% elseif 'DurationType' in type %}
|
||||||
{{ value|duration }}
|
{{ value|duration }}
|
||||||
{% elseif 'YesNoType' in type or 'CheckBoxType' in type %}
|
{% elseif 'YesNoType' in type or 'CheckboxType' in type %}
|
||||||
{{ _self.label_boolean(value) }}
|
{{ _self.label_boolean(value) }}
|
||||||
{% elseif 'DatePickerType' in type %}
|
{% elseif 'DatePickerType' in type %}
|
||||||
{{ value|date_short }}
|
{{ value|date_short }}
|
||||||
|
|||||||
@@ -54,9 +54,9 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for id, week in days %}
|
{% 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 %}
|
{% endfor %}
|
||||||
<td class="text-center" id="qe-totals-week"></td>
|
<td class="text-center total" id="qe-totals-week"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -66,25 +66,15 @@
|
|||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
document.addEventListener('kimai.initialized', function(event) {
|
document.addEventListener('kimai.initialized', function(event) {
|
||||||
const kimai = event.detail.kimai;
|
const kimai = event.detail.kimai;
|
||||||
const DATES = kimai.getPlugin('date');
|
const DATES = kimai.getPlugin('date');
|
||||||
/** @type {KimaiFormSelect} FORM_SELECT */
|
/** @type {KimaiFormSelect} FORM_SELECT */
|
||||||
const FORM_SELECT = kimai.getPlugin('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')) {
|
if (window.matchMedia("(max-width: 1360px)").matches) {
|
||||||
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';
|
|
||||||
}
|
|
||||||
document.querySelector('form[name=quick_entry_form] div.maybe-table-responsive').classList.replace('maybe-table-responsive', 'table-responsive');
|
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);
|
collectionHolder.appendChild(node);
|
||||||
|
|
||||||
|
|
||||||
[].slice.call(node.querySelectorAll('.selectpicker')).map((element) => {
|
[].slice.call(node.querySelectorAll('.selectpicker')).map((element) => {
|
||||||
FORM_SELECT.activateSelectPickerByElement(element);
|
FORM_SELECT.activateSelectPickerByElement(element);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
{% if user.totpAuthenticationEnabled %}
|
{% if user.totpAuthenticationEnabled %}
|
||||||
{{ form_start(deactivate, {'attr': {'id': 'user_two_factor_form'}}) }}
|
{{ 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) }}
|
{{ form_end(deactivate) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" id="user_two_factor_button" />
|
<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/',
|
0 => 'var/export/',
|
||||||
1 => 'templates/export/renderer/',
|
1 => 'templates/export/renderer/',
|
||||||
],
|
],
|
||||||
|
'timeout' => 60,
|
||||||
],
|
],
|
||||||
'calendar' => [
|
'calendar' => [
|
||||||
'week_numbers' => true,
|
'week_numbers' => true,
|
||||||
|
|||||||
@@ -54,10 +54,28 @@ class ExportTemplateTest extends AbstractEntityTestCase
|
|||||||
|
|
||||||
$sut->setOptions(['foo' => 1, 'bar' => true, 'WORLD' => 'HELLO']);
|
$sut->setOptions(['foo' => 1, 'bar' => true, 'WORLD' => 'HELLO']);
|
||||||
self::assertEquals(['foo' => 1, 'bar' => true, 'WORLD' => 'HELLO'], $sut->getOptions());
|
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);
|
$sut->setOptions(null);
|
||||||
self::assertEquals([], $sut->getOptions());
|
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
|
public function testClone(): void
|
||||||
{
|
{
|
||||||
$sut = new ExportTemplate();
|
$sut = new ExportTemplate();
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ class CsvRendererTest extends AbstractRendererTestCase
|
|||||||
{
|
{
|
||||||
$en = [
|
$en = [
|
||||||
'Date', 'From', 'To', 'Duration', 'Currency', 'Price', 'Internal price', 'Hourly price', 'Fixed price', 'Name',
|
'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',
|
'Type', 'category', 'Account', 'Project number', 'VAT-ID', 'Order number',
|
||||||
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
||||||
];
|
];
|
||||||
$de = [
|
$de = [
|
||||||
'Datum', 'Von', 'Bis', 'Dauer', 'Währung', 'Preis', 'Interner Preis', 'Preis pro Stunde', 'Festpreis', 'Name',
|
'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',
|
'Typ', 'category', 'Kundennummer', 'Projektnummer', 'Umsatzsteuer-ID', 'Bestellnummer',
|
||||||
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'Working place', 'mypref',
|
||||||
];
|
];
|
||||||
@@ -140,6 +140,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
|||||||
'Kevin',
|
'Kevin',
|
||||||
'kevin',
|
'kevin',
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
'Customer Name',
|
'Customer Name',
|
||||||
'project name',
|
'project name',
|
||||||
'activity description',
|
'activity description',
|
||||||
@@ -175,6 +176,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
|||||||
'niveK',
|
'niveK',
|
||||||
'nivek',
|
'nivek',
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
'Customer Name',
|
'Customer Name',
|
||||||
'project name',
|
'project name',
|
||||||
'activity description',
|
'activity description',
|
||||||
@@ -200,6 +202,6 @@ class CsvRendererTest extends AbstractRendererTestCase
|
|||||||
self::assertEquals($expected, $all[5]);
|
self::assertEquals($expected, $all[5]);
|
||||||
self::assertEquals($expected2, $all[6]);
|
self::assertEquals($expected2, $all[6]);
|
||||||
self::assertEquals(\count($expected), \count($all[0]));
|
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',
|
'project.meta.project-foo2' => 'Working place',
|
||||||
'activity.meta.activity-foo' => 'Working place',
|
'activity.meta.activity-foo' => 'Working place',
|
||||||
'user.meta.mypref' => 'mypref',
|
'user.meta.mypref' => 'mypref',
|
||||||
|
'email' => 'email'
|
||||||
]];
|
]];
|
||||||
|
|
||||||
$template = new Template('test', 'Testing');
|
$template = new Template('test', 'Testing');
|
||||||
|
|||||||
Reference in New Issue
Block a user