save default search options (#2445)

* set default times for daterange objects
* allow to show order and order by fields
* move search to modal
* more options for page size
* save export visibility in cookie
This commit is contained in:
Kevin Papst
2021-03-20 01:10:45 +01:00
committed by GitHub
parent 954ba7c937
commit 87d07ffaaf
83 changed files with 903 additions and 410 deletions

View File

@@ -92,6 +92,7 @@ require('./sass/app.scss');
require('./js/KimaiWebLoader.js'); require('./js/KimaiWebLoader.js');
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default; global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default; global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
global.KimaiCookies = require('./js/widgets/KimaiCookies').default;
// ------ Autocomplete for tags only ------ // ------ Autocomplete for tags only ------
require('jquery-ui/ui/widgets/autocomplete'); require('jquery-ui/ui/widgets/autocomplete');

View File

@@ -60,7 +60,7 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]')); kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]'));
kimai.registerPlugin(new KimaiDatePicker('input[data-datepickerenable="on"]')); kimai.registerPlugin(new KimaiDatePicker('input[data-datepickerenable="on"]'));
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable')); kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
kimai.registerPlugin(new KimaiToolbar('form.header-search', 'toolbar-action')); kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]')); kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link')); kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form')); kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));

View File

@@ -152,7 +152,6 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
}); });
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
this.getContainer().getPlugin('toolbar').hide();
remoteModal.modal('show'); remoteModal.modal('show');
// the new form that was loaded via ajax // the new form that was loaded via ajax

View File

@@ -48,12 +48,8 @@ export default class KimaiDatatable extends KimaiPlugin {
document.addEventListener(eventName, handle); document.addEventListener(eventName, handle);
} }
if (this.getConfiguration('autoReloadDatatable')) { document.addEventListener('pagination-change', handle);
document.addEventListener('toolbar-change', handle); document.addEventListener('filter-change', handle);
} else {
document.addEventListener('pagination-change', handle);
document.addEventListener('filter-change', handle);
}
} }
reloadDatatable() { reloadDatatable() {
@@ -69,7 +65,7 @@ export default class KimaiDatatable extends KimaiPlugin {
// remove the empty fields to prevent errors // remove the empty fields to prevent errors
let formData = jQuery(toolbarSelector + ' :input') let formData = jQuery(toolbarSelector + ' :input')
.filter(function(index, element) { .filter(function(index, element) {
return jQuery(element).val() != ''; return jQuery(element).val() !== '';
}) })
.serialize(); .serialize();

View File

@@ -55,7 +55,7 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) { for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
settings[checkbox.getAttribute('name')] = checkbox.checked; settings[checkbox.getAttribute('name')] = checkbox.checked;
} }
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365}); Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365, SameSite: 'Strict'});
jQuery(this.modal).modal('toggle'); jQuery(this.modal).modal('toggle');
} }

View File

@@ -23,6 +23,7 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
jQuery('[data-toggle="tooltip"]').tooltip(); jQuery('[data-toggle="tooltip"]').tooltip();
// activate all form plugins // activate all form plugins
this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body'); this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body');
this.getContainer().getPlugin('form').activateForm('form.searchform', 'body');
} }
/** /**

View File

@@ -32,30 +32,6 @@ export default class KimaiToolbar extends KimaiPlugin {
this._registerPagination(formSelector, EVENT); this._registerPagination(formSelector, EVENT);
this._registerSortableTables(formSelector, EVENT); this._registerSortableTables(formSelector, EVENT);
this._registerAlternativeSubmitActions(formSelector, this.actionClass); this._registerAlternativeSubmitActions(formSelector, this.actionClass);
this._registerSearchButtons(formSelector);
jQuery('body')
// prevent that the dropdown closes, when a form input is changed - eg. a select option was clicked
.on('click', formSelector + ' .dropdown-menu', function (event) {
event.stopPropagation();
})
// prevent that a click into the search field will close the dropdown
.on('click', '.select2-search__field', function (event) {
event.stopPropagation();
})
// prevent that the dropdown closes when a optgroup header is clicked
.on('click', '.select2-results__group', function (event) {
event.stopPropagation();
})
// prevent that a click into a daterangepicker will close the dropdown
.on('click', '.daterangepicker', function (event) {
event.stopPropagation();
})
// prevent that clicks in the dropdown elements, but outside of elements will close the dropdown (eg border besides the search field)
.on('click', '.select2-container', function (event) {
event.stopPropagation();
})
;
// Reset the page if filter values are changed, otherwise we might end up with a limited set of data, // Reset the page if filter values are changed, otherwise we might end up with a limited set of data,
// which does not support the given page - and it would be just wrong to stay in the same page // which does not support the given page - and it would be just wrong to stay in the same page
@@ -95,38 +71,6 @@ export default class KimaiToolbar extends KimaiPlugin {
self.triggerChange(); self.triggerChange();
} }
}); });
// close all open selectpicker upon choosing any dropdown option
jQuery(formSelector + ' select.selectpicker').on('change', function(event) {
jQuery('.bootstrap-select.open').removeClass('open');
});
}
/**
* The search toggle button is not part of this component, but it is directly connected to it.
* @private
*/
_registerSearchButtons(formSelector) {
jQuery('body')
// only for mobile experience currently: show the search form field
.on('click', '.btn-search.search-toggle', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector).toggleClass('hidden-xs');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector + ' input#searchTerm').focus();
})
// hide the search form field
.on('click', formSelector + ' a.search-cancel', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector).toggleClass('hidden-xs');
})
;
} }
/** /**
@@ -175,10 +119,14 @@ export default class KimaiToolbar extends KimaiPlugin {
if ($header.hasClass('sorting_desc')) { if ($header.hasClass('sorting_desc')) {
order = 'ASC'; order = 'ASC';
} }
jQuery(formSelector + ' input#orderBy').val(orderBy);
jQuery(formSelector + ' input#order').val(order); jQuery(formSelector + ' #orderBy').val(orderBy);
// triggers the page reset - see below jQuery(formSelector + ' #order').val(order);
jQuery(formSelector + ' input#order').trigger('change');
// re-render the selectboxes
jQuery(formSelector + ' #orderBy').trigger('change');
jQuery(formSelector + ' #order').trigger('change');
// triggers the datatable reload - search for the event name // triggers the datatable reload - search for the event name
EVENT.trigger('filter-change'); EVENT.trigger('filter-change');
}); });
@@ -206,13 +154,6 @@ export default class KimaiToolbar extends KimaiPlugin {
} }
hide() {
const toggle = jQuery(this.getSelector() + ' #searchTerm.dropdown-toggle');
if (toggle.parent().hasClass('open')) {
toggle.dropdown('toggle');
}
}
/** /**
* Triggers an event, that everyone can listen for. * Triggers an event, that everyone can listen for.
*/ */

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiCookies: simple wrapper to handle cookies
*/
import Cookies from 'js-cookie';
export default class KimaiCookies {
static set(name, values, options) {
Cookies.set(name, values, options);
}
static get(name) {
return Cookies.getJSON(name);
}
static remove(name) {
Cookies.remove(name);
}
}

View File

@@ -6,3 +6,10 @@ form fieldset {
background-color: $highlight-bg; background-color: $highlight-bg;
} }
} }
/* To be used with .form-horizontal - to reduce the space between rows, eg. used in the search modals */
form.form-narrow {
.form-group {
margin: 0 0 5px 0;
}
}

View File

@@ -9,10 +9,9 @@
padding: 10px; padding: 10px;
} }
/* The filter form is available on most pages above the datatable */
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.toolbar { .toolbar {
// @deprecated since 1.2 - using the toolbar is deprecated and will be removed with 2.0 // @deprecated since 1.2 - using the navbar-form is deprecated and will be removed with 2.0
form.navbar-form { form.navbar-form {
font-size: $font-size-base; font-size: $font-size-base;
.form-control { .form-control {
@@ -32,59 +31,3 @@
padding: 10px 0; padding: 10px 0;
} }
} }
/* Search form with filter dropdown */
.content-header {
form {
width: 200px;
float: left;
.container-fluid {
padding-left: 0;
padding-right: 0;
}
.form-group {
margin: 0 0 5px 0;
}
input.search-has-error {
color: $red;
}
.input-group-addon.has-error {
color: $red;
border-color: $red;
}
input.has-error {
border-color: $red;
}
ul.dropdown-menu {
max-height: 100vh;
overflow-y: auto;
padding-top: 10px;
width: 500px;
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
max-width: 90vw;
}
a.search-cancel {
margin-top: 8px;
}
}
}
@media (max-width: $screen-xs-min) {
.content-header.search-open {
h1 {
display: none;
}
.breadcrumb {
display: none;
}
form.header-search {
padding: 0 10px 0 0;
float: none;
width: 100%;
ul.dropdown-menu {
width: 100%;
max-width: 100%;
}
}
}
}

View File

@@ -240,3 +240,8 @@ services:
class: Doctrine\ORM\EntityRepository class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository] factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Invoice'] arguments: ['App\Entity\Invoice']
App\Repository\BookmarkRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Bookmark']

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

File diff suppressed because one or more lines are too long

View File

@@ -5,10 +5,10 @@
"build/runtime.098eaae1.js", "build/runtime.098eaae1.js",
"build/0.79dbdbb9.js", "build/0.79dbdbb9.js",
"build/1.32489d92.js", "build/1.32489d92.js",
"build/app.f451ae28.js" "build/app.b19b6399.js"
], ],
"css": [ "css": [
"build/app.7c0ca7bb.css" "build/app.b22111d1.css"
] ]
}, },
"invoice": { "invoice": {
@@ -53,8 +53,8 @@
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd", "build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5", "build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP", "build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
"build/app.f451ae28.js": "sha384-uFHnZPiRrWKf2q8N4zNE1YguKVtOXbES0+rKlehQAj7bIDvCHOcpVo8djMU6VtE6", "build/app.b19b6399.js": "sha384-giiZt81GRzPg3SJzQLRg7+bLAhpzPPP0XCqzZBcznH/JfE3Z3wQElGPKuJR84RXl",
"build/app.7c0ca7bb.css": "sha384-0I0CpC2eA9LQewNIo/c6Qkhra9sOsqadi0bi9DfC2YWQxBi+ZRYAl4BVt9jMOyGE", "build/app.b22111d1.css": "sha384-ZMJkA2sXnH7J/uMTKDBf8+bLNHulT+yZiz1h5fxzxJshqw9Ccs3brjAJPqb+0/6S",
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f", "build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh", "build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
"build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC", "build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",

View File

@@ -2,8 +2,8 @@
"build/0.79dbdbb9.js": "build/0.79dbdbb9.js", "build/0.79dbdbb9.js": "build/0.79dbdbb9.js",
"build/1.32489d92.js": "build/1.32489d92.js", "build/1.32489d92.js": "build/1.32489d92.js",
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js", "build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
"build/app.css": "build/app.7c0ca7bb.css", "build/app.css": "build/app.b22111d1.css",
"build/app.js": "build/app.f451ae28.js", "build/app.js": "build/app.b19b6399.js",
"build/calendar.css": "build/calendar.1408f57e.css", "build/calendar.css": "build/calendar.1408f57e.css",
"build/calendar.js": "build/calendar.070aab88.js", "build/calendar.js": "build/calendar.070aab88.js",
"build/chart.js": "build/chart.34d60a88.js", "build/chart.js": "build/chart.34d60a88.js",

View File

@@ -23,7 +23,9 @@ class ThemeConfiguration implements SystemBundleConfiguration, \ArrayAccess
public function isAutoReloadDatatable(): bool public function isAutoReloadDatatable(): bool
{ {
return (bool) $this->find('auto_reload_datatable'); @trigger_error('The configuration auto_reload_datatable is deprecated and was removed with 1.4', E_USER_DEPRECATED);
return false;
} }
public function isAllowTagCreation(): bool public function isAllowTagCreation(): bool

View File

@@ -10,11 +10,16 @@
namespace App\Controller; namespace App\Controller;
use App\Configuration\LanguageFormattings; use App\Configuration\LanguageFormattings;
use App\Entity\Bookmark;
use App\Entity\User; use App\Entity\User;
use App\Repository\BookmarkRepository;
use App\Repository\Query\BaseQuery;
use App\Timesheet\DateTimeFactory; use App\Timesheet\DateTimeFactory;
use App\Utils\LocaleFormats; use App\Utils\LocaleFormats;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\Translation\DataCollectorTranslator;
use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -167,4 +172,72 @@ abstract class AbstractController extends BaseAbstractController implements Serv
{ {
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale); return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
} }
protected function handleSearch(FormInterface $form, Request $request): bool
{
$data = $form->getData();
if (!($data instanceof BaseQuery)) {
throw new \InvalidArgumentException('handleSearchForm() requires an instanceof BaseQuery as form data');
}
/** @var BookmarkRepository $bookmarkRepo */
$bookmarkRepo = $this->getDoctrine()->getRepository(Bookmark::class);
$bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $data->getName());
$submitData = $request->query->all();
// remove bookmark
if ($bookmark !== null && $request->query->has('removeDefaultQuery')) {
$bookmarkRepo->deleteBookmark($bookmark);
return true;
}
// apply bookmark ONLY if search form was not submitted manually
if ($bookmark !== null && !$request->query->has('performSearch')) {
$data->setBookmark($bookmark);
$submitData = array_merge($bookmark->getContent(), $submitData);
}
// clean up parameters from unknown search values
foreach ($submitData as $name => $values) {
if (!$form->has($name)) {
unset($submitData[$name]);
}
}
$form->submit($submitData, false);
if (!$form->isValid()) {
$data->resetByFormError($form->getErrors());
}
if ($request->query->has('setDefaultQuery')) {
$params = [];
foreach ($form->all() as $name => $child) {
$params[$name] = $child->getViewData();
}
$filter = ['page', 'setDefaultQuery', 'removeDefaultQuery', 'performSearch'];
foreach ($filter as $name) {
if (isset($params[$name])) {
unset($params[$name]);
}
}
if ($bookmark === null) {
$bookmark = new Bookmark();
$bookmark->setType(Bookmark::SEARCH_DEFAULT);
$bookmark->setUser($this->getUser());
$bookmark->setName(substr($data->getName(), 0, 50));
}
$bookmark->setContent($params);
$bookmarkRepo->saveBookmark($bookmark);
return true;
}
return false;
}
} }

View File

@@ -31,7 +31,6 @@ use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\ActivityQuery; use App\Repository\Query\ActivityQuery;
use App\Repository\TeamRepository; use App\Repository\TeamRepository;
use Exception; use Exception;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -77,14 +76,10 @@ final class ActivityController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('admin_activity');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
/* @var $entries Pagerfanta */
$entries = $this->repository->getPagerfantaForQuery($query); $entries = $this->repository->getPagerfantaForQuery($query);
return $this->render('activity/index.html.twig', [ return $this->render('activity/index.html.twig', [

View File

@@ -76,11 +76,8 @@ final class CustomerController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('admin_customer');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
$entries = $this->repository->getPagerfantaForQuery($query); $entries = $this->repository->getPagerfantaForQuery($query);

View File

@@ -88,14 +88,10 @@ final class ProjectController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('admin_project');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
/* @var $entries Pagerfanta */
$entries = $this->repository->getPagerfantaForQuery($query); $entries = $this->repository->getPagerfantaForQuery($query);
return $this->render('project/index.html.twig', [ return $this->render('project/index.html.twig', [

View File

@@ -429,14 +429,6 @@ final class SystemConfigurationController extends AbstractController
->setLabel('theme.tags_create') ->setLabel('theme.tags_create')
->setType(CheckboxType::class) ->setType(CheckboxType::class)
->setTranslationDomain('system-configuration'), ->setTranslationDomain('system-configuration'),
// TODO should that be configurable per user?
/*
(new Configuration())
->setName('theme.auto_reload_datatable')
->setLabel('theme.auto_reload_datatable') // TODO translation
->setType(CheckboxType::class)
->setTranslationDomain('system-configuration'),
*/
]), ]),
(new SystemConfigurationModel()) (new SystemConfigurationModel())
->setSection(SystemConfigurationModel::SECTION_CALENDAR) ->setSection(SystemConfigurationModel::SECTION_CALENDAR)

View File

@@ -43,11 +43,8 @@ class TagController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('tags');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
$tags = $repository->getTagCount($query); $tags = $repository->getTagCount($query);

View File

@@ -55,11 +55,8 @@ final class TeamController extends AbstractController
$query->setCurrentUser($this->getUser()); $query->setCurrentUser($this->getUser());
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('admin_team');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
$teams = $repository->getPagerfantaForQuery($query); $teams = $repository->getPagerfantaForQuery($query);

View File

@@ -77,24 +77,11 @@ abstract class TimesheetAbstractController extends AbstractController
return $this->service->getActiveTrackingMode(); return $this->service->getActiveTrackingMode();
} }
protected function index($page, Request $request, string $renderTemplate, string $location): Response protected function index(TimesheetQuery $query, Request $request, string $route, string $renderTemplate, string $location): Response
{ {
$query = new TimesheetQuery();
$query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute($route);
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
}
if (null !== $query->getBegin()) {
$query->getBegin()->setTime(0, 0, 0);
}
if (null !== $query->getEnd()) {
$query->getEnd()->setTime(23, 59, 59);
} }
$tags = $query->getTags(true); $tags = $query->getTags(true);
@@ -486,11 +473,7 @@ abstract class TimesheetAbstractController extends AbstractController
]); ]);
} }
/** protected function getToolbarForm(TimesheetQuery $query): FormInterface
* @param TimesheetQuery $query
* @return FormInterface
*/
protected function getToolbarForm(TimesheetQuery $query)
{ {
return $this->createForm(TimesheetToolbarForm::class, $query, [ return $this->createForm(TimesheetToolbarForm::class, $query, [
'action' => $this->generateUrl($this->getTimesheetRoute(), [ 'action' => $this->generateUrl($this->getTimesheetRoute(), [

View File

@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
use App\Event\TimesheetMetaDisplayEvent; use App\Event\TimesheetMetaDisplayEvent;
use App\Repository\ActivityRepository; use App\Repository\ActivityRepository;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository; use App\Repository\TagRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -36,7 +37,11 @@ class TimesheetController extends TimesheetAbstractController
*/ */
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
return $this->index($page, $request, 'timesheet/index.html.twig', TimesheetMetaDisplayEvent::TIMESHEET); $query = new TimesheetQuery();
$query->setPage($page);
$query->setName('MyTimesListing');
return $this->index($query, $request, 'timesheet', 'timesheet/index.html.twig', TimesheetMetaDisplayEvent::TIMESHEET);
} }
/** /**

View File

@@ -45,7 +45,11 @@ class TimesheetTeamController extends TimesheetAbstractController
*/ */
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
return $this->index($page, $request, 'timesheet-team/index.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET); $query = new TimesheetQuery();
$query->setPage($page);
$query->setName('TeamTimesListing');
return $this->index($query, $request, 'admin_timesheet', 'timesheet-team/index.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);
} }
/** /**

View File

@@ -22,7 +22,6 @@ use App\Repository\Query\UserFormTypeQuery;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
@@ -78,14 +77,10 @@ final class UserController extends AbstractController
$query->setPage($page); $query->setPage($page);
$form = $this->getToolbarForm($query); $form = $this->getToolbarForm($query);
$form->setData($query); if ($this->handleSearch($form, $request)) {
$form->submit($request->query->all(), false); return $this->redirectToRoute('admin_user');
if (!$form->isValid()) {
$query->resetByFormError($form->getErrors());
} }
/* @var $entries Pagerfanta */
$entries = $this->getRepository()->getPagerfantaForQuery($query); $entries = $this->getRepository()->getPagerfantaForQuery($query);
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::USERS); $event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::USERS);

View File

@@ -404,9 +404,6 @@ class Configuration implements ConfigurationInterface
->defaultValue('selectpicker') ->defaultValue('selectpicker')
->setDeprecated() ->setDeprecated()
->end() ->end()
->booleanNode('auto_reload_datatable')
->defaultFalse()
->end()
->booleanNode('tags_create') ->booleanNode('tags_create')
->defaultTrue() ->defaultTrue()
->end() ->end()

120
src/Entity/Bookmark.php Normal file
View File

@@ -0,0 +1,120 @@
<?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\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Table(name="kimai2_bookmarks",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"user_id", "name"})
* }
* )
* @UniqueEntity(fields={"user", "name"})
* @ORM\Entity(repositoryClass="App\Repository\BookmarkRepository")
*/
class Bookmark
{
public const SEARCH_DEFAULT = 'search-default';
public const COLUMN_VISIBILITY = 'column-visibility';
public const TIMESHEET = 'timesheet';
/**
* @var int|null
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var User
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
* @Assert\NotNull()
*/
private $user;
/**
* @var string
* @ORM\Column(name="type", type="string", length=20, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=20, allowEmptyString=false)
*/
private $type;
/**
* @var string
* @ORM\Column(name="name", type="string", length=50, nullable=false)
* @Assert\NotBlank()
* @Assert\Length(min=2, max=50, allowEmptyString=false)
*/
private $name;
/**
* @var string
* @ORM\Column(name="content", type="text", nullable=false)
*/
private $content;
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getName(): ?string
{
return $this->name;
}
public function setType(string $type): void
{
$this->type = $type;
}
public function getType(): ?string
{
return $this->type;
}
public function setContent(array $content): void
{
$this->content = json_encode($content);
}
public function getContent(): array
{
if ($this->content === null) {
return [];
}
return json_decode($this->content, true);
}
public function __clone()
{
if ($this->id) {
$this->id = null;
}
}
}

View File

@@ -128,7 +128,7 @@ class PageActionsEvent extends ThemeEvent
public function addSearchToggle(): void public function addSearchToggle(): void
{ {
$this->addAction('search', ['class' => 'search-toggle visible-xs-inline']); $this->addAction('search', ['modal' => '#modal_search']);
} }
public function addQuickExport(string $url): void public function addQuickExport(string $url): void

View File

@@ -13,13 +13,7 @@ use DateTime;
final class DateRange final class DateRange
{ {
/**
* @var DateTime
*/
private $begin; private $begin;
/**
* @var DateTime
*/
private $end; private $end;
public function getBegin(): ?DateTime public function getBegin(): ?DateTime
@@ -30,6 +24,7 @@ final class DateRange
public function setBegin(DateTime $begin): DateRange public function setBegin(DateTime $begin): DateRange
{ {
$this->begin = $begin; $this->begin = $begin;
$this->begin->setTime(0, 0, 0);
return $this; return $this;
} }
@@ -42,6 +37,7 @@ final class DateRange
public function setEnd(DateTime $end): DateRange public function setEnd(DateTime $end): DateRange
{ {
$this->end = $end; $this->end = $end;
$this->end->setTime(23, 59, 59);
return $this; return $this;
} }

View File

@@ -300,6 +300,8 @@ abstract class AbstractToolbarForm extends AbstractType
protected function addHiddenOrder(FormBuilderInterface $builder) protected function addHiddenOrder(FormBuilderInterface $builder)
{ {
@trigger_error('addHiddenOrder() is deprecated and will be removed with 2.0, use the new search modal instead', E_USER_DEPRECATED);
$builder->add('order', HiddenType::class, [ $builder->add('order', HiddenType::class, [
'constraints' => [ 'constraints' => [
new Choice(['choices' => [BaseQuery::ORDER_ASC, BaseQuery::ORDER_DESC]]) new Choice(['choices' => [BaseQuery::ORDER_ASC, BaseQuery::ORDER_DESC]])
@@ -307,8 +309,21 @@ abstract class AbstractToolbarForm extends AbstractType
]); ]);
} }
protected function addOrder(FormBuilderInterface $builder)
{
$builder->add('order', ChoiceType::class, [
'label' => 'label.order',
'choices' => [
'label.asc' => BaseQuery::ORDER_ASC,
'label.desc' => BaseQuery::ORDER_DESC
]
]);
}
protected function addHiddenOrderBy(FormBuilderInterface $builder, array $allowedColumns) protected function addHiddenOrderBy(FormBuilderInterface $builder, array $allowedColumns)
{ {
@trigger_error('addHiddenOrderBy() is deprecated and will be removed with 2.0, use the new search modal instead', E_USER_DEPRECATED);
$builder->add('orderBy', HiddenType::class, [ $builder->add('orderBy', HiddenType::class, [
'constraints' => [ 'constraints' => [
new Choice(['choices' => $allowedColumns]) new Choice(['choices' => $allowedColumns])
@@ -316,6 +331,19 @@ abstract class AbstractToolbarForm extends AbstractType
]); ]);
} }
protected function addOrderBy(FormBuilderInterface $builder, array $allowedColumns)
{
$all = [];
foreach ($allowedColumns as $id => $name) {
$label = \is_int($id) ? 'label.' . $name : $id;
$all[$label] = $name;
}
$builder->add('orderBy', ChoiceType::class, [
'label' => 'label.orderBy',
'choices' => $all
]);
}
protected function addTagInputField(FormBuilderInterface $builder) protected function addTagInputField(FormBuilderInterface $builder)
{ {
$builder->add('tags', TagsType::class, [ $builder->add('tags', TagsType::class, [

View File

@@ -45,8 +45,8 @@ class ActivityToolbarForm extends AbstractToolbarForm
$this->addVisibilityChoice($builder); $this->addVisibilityChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, ActivityQuery::ACTIVITY_ORDER_ALLOWED); $this->addOrderBy($builder, ActivityQuery::ACTIVITY_ORDER_ALLOWED);
} }
/** /**

View File

@@ -27,8 +27,8 @@ class CustomerToolbarForm extends AbstractToolbarForm
$this->addVisibilityChoice($builder); $this->addVisibilityChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, CustomerQuery::CUSTOMER_ORDER_ALLOWED); $this->addOrderBy($builder, CustomerQuery::CUSTOMER_ORDER_ALLOWED);
} }
/** /**

View File

@@ -28,8 +28,8 @@ class ProjectToolbarForm extends AbstractToolbarForm
$this->addVisibilityChoice($builder); $this->addVisibilityChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, ProjectQuery::PROJECT_ORDER_ALLOWED); $this->addOrderBy($builder, ProjectQuery::PROJECT_ORDER_ALLOWED);
} }
/** /**

View File

@@ -23,8 +23,8 @@ class TagToolbarForm extends AbstractToolbarForm
$this->addSearchTermInputField($builder); $this->addSearchTermInputField($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, TagQuery::TAG_ORDER_ALLOWED); $this->addOrderBy($builder, TagQuery::TAG_ORDER_ALLOWED);
} }
/** /**

View File

@@ -24,8 +24,8 @@ class TeamToolbarForm extends AbstractToolbarForm
$this->addSearchTermInputField($builder); $this->addSearchTermInputField($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, TeamQuery::TEAM_ORDER_ALLOWED); $this->addOrderBy($builder, TeamQuery::TEAM_ORDER_ALLOWED);
} }
/** /**

View File

@@ -41,8 +41,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
$this->addExportStateChoice($builder); $this->addExportStateChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED); $this->addOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED);
} }
/** /**

View File

@@ -28,8 +28,8 @@ class UserToolbarForm extends AbstractToolbarForm
$this->addVisibilityChoice($builder, 'label.active'); $this->addVisibilityChoice($builder, 'label.active');
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addHiddenOrder($builder); $this->addOrder($builder);
$this->addHiddenOrderBy($builder, UserQuery::USER_ORDER_ALLOWED); $this->addOrderBy($builder, UserQuery::USER_ORDER_ALLOWED);
} }
/** /**

View File

@@ -27,13 +27,28 @@ class PageSizeType extends AbstractType
'label' => 'label.pageSize', 'label' => 'label.pageSize',
'choices' => [ 'choices' => [
10 => 10, 10 => 10,
15 => 15,
20 => 20,
25 => 25, 25 => 25,
30 => 30,
35 => 35,
40 => 40,
45 => 45,
50 => 50, 50 => 50,
75 => 75, 60 => 60,
70 => 70,
80 => 80,
90 => 90,
100 => 100, 100 => 100,
125 => 125,
150 => 150,
175 => 175,
200 => 200, 200 => 200,
250 => 250,
300 => 300, 300 => 300,
350 => 350,
400 => 400, 400 => 400,
450 => 450,
500 => 500 500 => 500
], ],
'placeholder' => null, 'placeholder' => null,

View File

@@ -33,7 +33,7 @@ class SearchTermType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'label' => 'search', 'label' => 'label.searchTerm',
'required' => false, 'required' => false,
'constraints' => [ 'constraints' => [
new Length(['min' => 3, 'allowEmptyString' => true]) new Length(['min' => 3, 'allowEmptyString' => true])

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* 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 DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Create the bookmark table, to store default search settings.
*
* @version 1.14
*/
final class Version20210316224358 extends AbstractMigration
{
public function getDescription(): string
{
return 'Adds the bookmark table';
}
public function up(Schema $schema): void
{
$bookmarks = $schema->createTable('kimai2_bookmarks');
$bookmarks->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]);
$bookmarks->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => true]);
$bookmarks->addColumn('type', 'string', ['length' => 20, 'notnull' => true]);
$bookmarks->addColumn('name', 'string', ['length' => 50, 'notnull' => true]);
$bookmarks->addColumn('content', 'text', ['notnull' => true]);
$bookmarks->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_4016EF25A76ED395');
$bookmarks->addUniqueIndex(['user_id', 'name'], 'UNIQ_4016EF25A76ED3955E237E06');
$bookmarks->setPrimaryKey(['id']);
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE kimai2_bookmarks');
}
}

View File

@@ -0,0 +1,52 @@
<?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\Repository;
use App\Entity\Bookmark;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException;
/**
* @extends \Doctrine\ORM\EntityRepository<Bookmark>
*/
class BookmarkRepository extends EntityRepository
{
public function saveBookmark(Bookmark $bookmark)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($bookmark);
$entityManager->flush();
}
public function deleteBookmark(Bookmark $bookmark)
{
$em = $this->getEntityManager();
$em->beginTransaction();
try {
$em->remove($bookmark);
$em->flush();
$em->commit();
} catch (ORMException $ex) {
$em->rollback();
throw $ex;
}
}
public function getSearchDefaultOptions(User $user, string $name): ?Bookmark
{
return $this->findOneBy([
'user' => $user->getId(),
'type' => Bookmark::SEARCH_DEFAULT,
'name' => substr($name, 0, 50)
]);
}
}

View File

@@ -229,7 +229,16 @@ class CustomerRepository extends EntityRepository
->from(Customer::class, 'c') ->from(Customer::class, 'c')
; ;
$qb->orderBy('c.' . $query->getOrderBy(), $query->getOrder()); $orderBy = $query->getOrderBy();
switch ($orderBy) {
case 'vat_id':
$orderBy = 'c.vatId';
break;
default:
$orderBy = 'c.' . $orderBy;
break;
}
$qb->orderBy($orderBy, $query->getOrder());
if ($query->isShowVisible()) { if ($query->isShowVisible()) {
$qb->andWhere($qb->expr()->eq('c.visible', ':visible')); $qb->andWhere($qb->expr()->eq('c.visible', ':visible'));

View File

@@ -271,6 +271,12 @@ class ProjectRepository extends EntityRepository
case 'customer': case 'customer':
$orderBy = 'c.name'; $orderBy = 'c.name';
break; break;
case 'project_start':
$orderBy = 'p.start';
break;
case 'project_end':
$orderBy = 'p.end';
break;
default: default:
$orderBy = 'p.' . $orderBy; $orderBy = 'p.' . $orderBy;
break; break;

View File

@@ -16,7 +16,7 @@ use App\Entity\Project;
*/ */
class ActivityQuery extends ProjectQuery class ActivityQuery extends ProjectQuery
{ {
public const ACTIVITY_ORDER_ALLOWED = ['id', 'name', 'comment', 'customer', 'project', 'visible']; public const ACTIVITY_ORDER_ALLOWED = ['id', 'name', 'comment', 'customer', 'project', 'budget', 'timeBudget', 'visible'];
/** /**
* @var Project[]|int[] * @var Project[]|int[]

View File

@@ -9,6 +9,7 @@
namespace App\Repository\Query; namespace App\Repository\Query;
use App\Entity\Bookmark;
use App\Entity\Team; use App\Entity\Team;
use App\Entity\User; use App\Entity\User;
use App\Utils\SearchTerm; use App\Utils\SearchTerm;
@@ -78,6 +79,14 @@ class BaseQuery
* @var SearchTerm|null * @var SearchTerm|null
*/ */
private $searchTerm; private $searchTerm;
/**
* @var Bookmark
*/
private $bookmark;
/**
* @var string|null
*/
private $name;
/** /**
* @param Team[] $teams * @param Team[] $teams
@@ -274,6 +283,32 @@ class BaseQuery
return $this; return $this;
} }
public function setBookmark(Bookmark $bookmark): void
{
$this->bookmark = $bookmark;
}
public function getBookmark(): ?Bookmark
{
return $this->bookmark;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getName(): string
{
if (null !== $this->name) {
return $this->name;
}
$shortClass = explode('\\', static::class);
return array_pop($shortClass);
}
public function copyTo(BaseQuery $query): BaseQuery public function copyTo(BaseQuery $query): BaseQuery
{ {
$query->setDefaults($this->defaults); $query->setDefaults($this->defaults);

View File

@@ -18,7 +18,7 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
public const CUSTOMER_ORDER_ALLOWED = [ public const CUSTOMER_ORDER_ALLOWED = [
'id', 'name', 'comment', 'country', 'number', 'homepage', 'email', 'mobile', 'fax', 'id', 'name', 'comment', 'country', 'number', 'homepage', 'email', 'mobile', 'fax',
'phone', 'currency', 'address', 'contact', 'company', 'vatId', 'visible' 'phone', 'currency', 'address', 'contact', 'company', 'vat_id', 'budget', 'timeBudget', 'visible'
]; ];
public function __construct() public function __construct()

View File

@@ -19,8 +19,7 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
use VisibilityTrait; use VisibilityTrait;
public const PROJECT_ORDER_ALLOWED = [ public const PROJECT_ORDER_ALLOWED = [
'id', 'name', 'comment', 'customer', 'orderNumber', 'projectStart', 'id', 'name', 'comment', 'customer', 'orderNumber', 'orderDate', 'project_start', 'project_end', 'budget', 'timeBudget', 'visible'
'projectEnd', 'orderNumber', 'orderDate', 'start', 'end', 'visible'
]; ];
/** /**

View File

@@ -42,6 +42,11 @@ final class ConfigExtension extends AbstractExtension
public function getThemeConfig(string $name) public function getThemeConfig(string $name)
{ {
switch ($name) { switch ($name) {
case 'auto_reload_datatable':
@trigger_error('The configuration auto_reload_datatable is deprecated and was removed with 1.4', E_USER_DEPRECATED);
return false;
case 'soft_limit': case 'soft_limit':
return $this->configuration->getTimesheetActiveEntriesSoftLimit(); return $this->configuration->getTimesheetActiveEntriesSoftLimit();

View File

@@ -25,6 +25,8 @@ final class IconExtension extends AbstractExtension
'avatar' => 'fas fa-user', 'avatar' => 'fas fa-user',
'back' => 'fas fa-long-arrow-alt-left', 'back' => 'fas fa-long-arrow-alt-left',
'barcode' => 'fas fa-barcode', 'barcode' => 'fas fa-barcode',
'bookmark' => 'far fa-star',
'bookmarked' => 'fas fa-star',
'calendar' => 'far fa-calendar-alt', 'calendar' => 'far fa-calendar-alt',
'clock' => 'far fa-clock', 'clock' => 'far fa-clock',
'comment' => 'far fa-comment', 'comment' => 'far fa-comment',

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% import "activity/actions.html.twig" as actions %} {% import "activity/actions.html.twig" as actions %}
{% set columns = { {% set columns = {
@@ -25,7 +25,7 @@
{% set tableName = 'activity_admin' %} {% set tableName = 'activity_admin' %}
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.activities('index') }}{% endblock %} {% block page_actions %}{{ actions.activities('index') }}{% endblock %}
{% block main_before %} {% block main_before %}

View File

@@ -11,6 +11,7 @@
{% block modal_body %}{% endblock %} {% block modal_body %}{% endblock %}
{% block modal_footer %}{% endblock %} {% block modal_footer %}{% endblock %}
{% endembed %} {% endembed %}
{% block page_search %}{% endblock %}
{% endblock %} {% endblock %}
{% block page_content_start %} {% block page_content_start %}
@@ -251,7 +252,6 @@
{% endblock %} {% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
{% block page_search %}{% endblock %}
{% block page_actions %}{% endblock %} {% block page_actions %}{% endblock %}
{% endblock %} {% endblock %}
@@ -280,7 +280,6 @@
locale: '{{ app.request.locale }}', locale: '{{ app.request.locale }}',
first_dow_iso: {{ iso_day_by_name(app.user.firstDayOfWeek) }}, first_dow_iso: {{ iso_day_by_name(app.user.firstDayOfWeek) }},
twentyFourHours: {{ 'true'|hour24('false') }}, twentyFourHours: {{ 'true'|hour24('false') }},
autoReloadDatatable: {% if theme_config('auto_reload_datatable') %}true{% else %}false{% endif %},
autoComplete: {{ theme_config('autocomplete_chars') }}, autoComplete: {{ theme_config('autocomplete_chars') }},
defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}', defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}',
updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %} updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% import "customer/actions.html.twig" as actions %} {% import "customer/actions.html.twig" as actions %}
{% set columns = { {% set columns = {
@@ -9,7 +9,7 @@
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans}, 'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
'number': {'class': 'hidden-xs hidden-sm hidden'}, 'number': {'class': 'hidden-xs hidden-sm hidden'},
'company': {'class': 'hidden-xs hidden-sm hidden'}, 'company': {'class': 'hidden-xs hidden-sm hidden'},
'vat_id': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': 'vatId'}, 'vat_id': {'class': 'hidden-xs hidden-sm hidden'},
'contact': {'class': 'hidden-xs hidden-sm hidden'}, 'contact': {'class': 'hidden-xs hidden-sm hidden'},
'address': {'class': 'hidden-xs hidden-sm hidden'}, 'address': {'class': 'hidden-xs hidden-sm hidden'},
'country': {'class': 'hidden-xs hidden-sm hidden'}, 'country': {'class': 'hidden-xs hidden-sm hidden'},
@@ -26,8 +26,8 @@
}) %} }) %}
{% endfor %} {% endfor %}
{% set columns = columns|merge({ {% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-center w-min', 'title': 'label.budget'|trans}, 'budget': {'class': 'hidden-xs hidden-sm hidden text-center w-min'},
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-center w-min', 'title': 'label.timeBudget'|trans}, 'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-center w-min'},
'team': {'class': 'text-center w-min', 'orderBy': false}, 'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'}, 'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'}, 'actions': {'class': 'actions alwaysVisible'},
@@ -36,7 +36,7 @@
{% set tableName = 'customer_admin' %} {% set tableName = 'customer_admin' %}
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.customers('index') }}{% endblock %} {% block page_actions %}{{ actions.customers('index') }}{% endblock %}
{% block main_before %} {% block main_before %}

View File

@@ -7,6 +7,9 @@
{{ encore_entry_link_tags('app') }} {{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }} {{ encore_entry_script_tags('app') }}
{% block styles %}{% endblock %} {% block styles %}{% endblock %}
{% if block('document_title') is defined %}
<title>{{ block('document_title') }}</title>
{% endif %}
</head> </head>
<body class="pad export_print"> <body class="pad export_print">
<div class="wrapper"> <div class="wrapper">

View File

@@ -2,6 +2,8 @@
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% extends 'export/layout.html.twig' %} {% extends 'export/layout.html.twig' %}
{% block document_title %}{{ 'export.title'|trans }}{% endblock %}
{% set showRateBudget = false %} {% set showRateBudget = false %}
{% set showTimeBudget = false %} {% set showTimeBudget = false %}
{% for budget in budgets %} {% for budget in budgets %}
@@ -57,12 +59,51 @@
{% block javascripts %} {% block javascripts %}
<script type="text/javascript"> <script type="text/javascript">
let initialized = false;
window.addEventListener('load', function() {
let config = KimaiCookies.get('kimai_html_export');
if (config !== undefined && config !== null) {
console.log(config);
for (const key in config) {
let elName = key;
let checked = config[key];
let el = document.getElementById(elName);
if (el === undefined || el === null) {
el = document.getElementById('records-column-' + elName);
if (el === undefined || el === null) {
continue;
}
}
if (checked !== el.checked) {
el.click();
}
}
}
initialized = true;
});
function saveVisibility()
{
if (!initialized) {
return;
}
const form = document.getElementsByTagName('form')[0];
let settings = {};
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
settings[checkbox.getAttribute('name')] = checkbox.checked;
}
KimaiCookies.set('kimai_html_export', settings, {expires: 365, SameSite: 'Strict'});
}
document.getElementById('summary-by-activities').addEventListener('click', function(event) { document.getElementById('summary-by-activities').addEventListener('click', function(event) {
document.getElementById('summary-project').style.display = event.target.checked ? 'none' : 'table'; document.getElementById('summary-project').style.display = event.target.checked ? 'none' : 'table';
document.getElementById('summary-activity').style.display = event.target.checked ? 'table' : 'none'; document.getElementById('summary-activity').style.display = event.target.checked ? 'table' : 'none';
saveVisibility();
}); });
document.getElementById('summary-show').addEventListener('click', function(event) { document.getElementById('summary-show').addEventListener('click', function(event) {
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none'; document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
saveVisibility();
}); });
{% if showTimeBudget %} {% if showTimeBudget %}
document.getElementById('summary-timeBudget').addEventListener('click', function(event) { document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
@@ -70,6 +111,7 @@
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
{% endif %} {% endif %}
{% if showRateBudget %} {% if showRateBudget %}
@@ -78,6 +120,7 @@
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
{% endif %} {% endif %}
document.getElementById('summary-duration').addEventListener('click', function(event) { document.getElementById('summary-duration').addEventListener('click', function(event) {
@@ -85,24 +128,29 @@
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
document.getElementById('summary-rate').addEventListener('click', function(event) { document.getElementById('summary-rate').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('summary-rate'); var cells = document.getElementsByClassName('summary-rate');
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
document.getElementById('summary-rate-internal').addEventListener('click', function(event) { document.getElementById('summary-rate-internal').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('summary-rate-internal'); var cells = document.getElementsByClassName('summary-rate-internal');
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
document.getElementById('summary-show').addEventListener('click', function(event) { document.getElementById('summary-show').addEventListener('click', function(event) {
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none'; document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
saveVisibility();
}); });
document.getElementById('records-show').addEventListener('click', function(event) { document.getElementById('records-show').addEventListener('click', function(event) {
document.getElementById('export-records').style.display = event.target.checked ? 'block' : 'none'; document.getElementById('export-records').style.display = event.target.checked ? 'block' : 'none';
saveVisibility();
}); });
var columnCheckboxes = document.getElementsByClassName('column-visibility-changer'); var columnCheckboxes = document.getElementsByClassName('column-visibility-changer');
for (var checkbox of columnCheckboxes) { for (var checkbox of columnCheckboxes) {
@@ -111,6 +159,7 @@
for (var columnCell of cells) { for (var columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none'; columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
} }
saveVisibility();
}); });
} }
</script> </script>
@@ -118,8 +167,6 @@
{% block styles %} {% block styles %}
<style> <style>
.items tr.summary {
}
.items td.totals { .items td.totals {
font-weight: bold; font-weight: bold;
text-align: right; text-align: right;
@@ -141,77 +188,81 @@
{% block export %} {% block export %}
<div class="row no-print toolbar"> <div class="row no-print toolbar">
<div class="col-xs-12"> <div class="col-xs-12">
<form class="navbar-form"> <form class="form-inline">
<div class="form-group"> <div class="row">
{{ 'export.summary'|trans }}: <div class="col-xs-1">
</div> <div class="form-group">
<div class="form-group"> <label class="control-label" for="summary-show">
<label class="control-label" for="summary-show"> <input type="checkbox" id="summary-show" name="summary-show" checked="checked">
<input type="checkbox" id="summary-show" name="summary-show" checked="checked"> {{ 'export.summary'|trans }}
{{ 'label.visible'|trans }} </label>
</label> </div>
</div>
<div class="form-group">
<label class="control-label" for="summary-by-activities">
<input type="checkbox" id="summary-by-activities" name="summary-by-activities">
{{ 'label.activity'|trans }}
</label>
</div>
{% if showTimeBudget %}
<div class="form-group">
<label class="control-label" for="summary-timeBudget">
<input type="checkbox" id="summary-timeBudget" name="summary-timeBudget" checked>
{{ 'label.timeBudget'|trans }}
</label>
</div>
{% endif %}
{% if showRateBudget %}
<div class="form-group">
<label class="control-label" for="summary-budget">
<input type="checkbox" id="summary-budget" name="summary-budget" checked>
{{ 'label.budget'|trans }}
</label>
</div>
{% endif %}
<div class="form-group">
<label class="control-label" for="summary-duration">
<input type="checkbox" id="summary-duration" name="summary-duration" checked>
{{ 'label.duration'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate-internal">
<input type="checkbox" id="summary-rate-internal" name="summary-rate-internal" checked>
{{ 'label.rate_internal'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate">
<input type="checkbox" id="summary-rate" name="summary-rate" checked>
{{ 'label.rate'|trans }}
</label>
</div>
</form>
<form class="navbar-form">
<div class="form-group">
{{ 'export.full_list'|trans }}:
</div>
<div class="form-group">
<label class="control-label" for="records-show">
<input type="checkbox" id="records-show" name="records-show" checked="checked">
{{ 'label.visible'|trans }}
</label>
</div>
{% for columnId, visibility in columns %}
<div class="form-group">
<label class="control-label" for="records-column-{{ columnId }}">
<input type="checkbox" class="column-visibility-changer" id="records-column-{{ columnId }}" name="{{ columnId }}" {% if visibility %}checked="checked"{% endif %}>
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
</label>
</div> </div>
{% endfor %} <div class="col-xs-11">
<div class="form-group">
<label class="control-label" for="summary-by-activities">
<input type="checkbox" id="summary-by-activities" name="summary-by-activities">
{{ 'label.activity'|trans }}
</label>
</div>
{% if showTimeBudget %}
<div class="form-group">
<label class="control-label" for="summary-timeBudget">
<input type="checkbox" id="summary-timeBudget" name="summary-timeBudget" checked>
{{ 'label.timeBudget'|trans }}
</label>
</div>
{% endif %}
{% if showRateBudget %}
<div class="form-group">
<label class="control-label" for="summary-budget">
<input type="checkbox" id="summary-budget" name="summary-budget" checked>
{{ 'label.budget'|trans }}
</label>
</div>
{% endif %}
<div class="form-group">
<label class="control-label" for="summary-duration">
<input type="checkbox" id="summary-duration" name="summary-duration" checked>
{{ 'label.duration'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate-internal">
<input type="checkbox" id="summary-rate-internal" name="summary-rate-internal" checked>
{{ 'label.rate_internal'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate">
<input type="checkbox" id="summary-rate" name="summary-rate" checked>
{{ 'label.rate'|trans }}
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-1">
<div class="form-group">
<label class="control-label" for="records-show">
<input type="checkbox" id="records-show" name="records-show" checked="checked">
{{ 'export.full_list'|trans }}
</label>
</div>
</div>
<div class="col-xs-11">
{% for columnId, visibility in columns %}
<div class="form-group">
<label class="control-label" for="records-column-{{ columnId }}">
<input type="checkbox" class="column-visibility-changer" id="records-column-{{ columnId }}" name="{{ columnId }}" {% if visibility %}checked="checked"{% endif %}>
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
</label>
</div>
{% endfor %}
</div>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,54 @@
{% macro searchModal(form) %}
{% form_theme form 'form/search-form.html.twig' %}
<div class="modal fade" id="modal_search" tabindex="-1" role="dialog" aria-labelledby="modal_search_label">
<div class="modal-dialog" role="document">
{{ form_start(form, {'attr': {'class': 'form-narrow searchform'}}) }}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal_search_label">
{{ 'search'|trans }}
</h4>
</div>
<div class="modal-body">
{% set orderBy = form_widget(form.orderBy) %}
{% set order = form_widget(form.order) %}
{{ form(form) }}
<div class="form-group">
{{ form_label(form.orderBy) }}
<div class="col-sm-5 col-xs-5">
{{ orderBy|raw }}
</div>
<div class="col-sm-4 col-xs-3">
{{ order|raw }}
</div>
</div>
</div>
<div class="modal-footer">
<div class="btn-toolbar pull-left" role="toolbar">
<div class="btn-group">
<button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
</div>
<div class="btn-group">
{% if form.vars.data.bookmark %}
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmarked'|icon }}"></i></button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="?removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'action.delete'|trans }}</a>
</li>
</ul>
{% else %}
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmark'|icon }}"></i></button>
{% endif %}
</div>
</div>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{% endmacro %}

View File

@@ -1,5 +1,5 @@
{% macro toolbar(form, collapsible, displayInitial) %} {% macro toolbar(form, collapsible, displayInitial) %}
{% deprecated 'The macro "toolbar" is deprecated since 1.2 and will be removed with 2.0, use dropDownSearch() instead' %} {% deprecated 'The macro "toolbar" is deprecated since 1.2 and will be removed with 2.0, use search.searchModal() instead' %}
<div class="toolbar no-print"> <div class="toolbar no-print">
{% if collapsible %}<div class="collapse{% if displayInitial %} collapse.show in{% endif %}" id="{{ collapsible }}">{% endif %} {% if collapsible %}<div class="collapse{% if displayInitial %} collapse.show in{% endif %}" id="{{ collapsible }}">{% endif %}
{{ form_start(form, { 'attr': {'class': 'navbar-form'}}) }} {{ form_start(form, { 'attr': {'class': 'navbar-form'}}) }}
@@ -10,45 +10,27 @@
{% endmacro %} {% endmacro %}
{% macro dropDownSearch(form) %} {% macro dropDownSearch(form) %}
{% form_theme form 'form/search-form.html.twig' %} {% deprecated 'The macro "dropDownSearch" is deprecated since 1.4 and will be removed with 2.0, use search.searchModal() instead' %}
{{ form_start(form, { 'attr': {'class': 'form-horizontal header-search hidden-xs'}}) }} <div class="modal fade" id="modal_search" tabindex="-1" role="dialog" aria-labelledby="modal_search_label">
{% set searchTermClass = 'dropdown-toggle input-sm' %} <div class="modal-dialog" role="document">
{% if not form.searchTerm.vars.valid %} {% form_theme form 'form/search-form.html.twig' %}
{% set searchTermClass = searchTermClass ~ ' search-has-error' %} {{ form_start(form, {'attr': {'class': 'form-narrow searchform'}}) }}
{% endif %} <div class="modal-content">
{% if not form.vars.valid %} <div class="modal-header">
{% set searchTermClass = searchTermClass ~ ' has-error' %} <button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
{% endif %} <h4 class="modal-title" id="modal_search_label">
<div class="dropdown"> {{ 'search'|trans }}
<div class="form-group"> </h4>
<div class="input-group"> </div>
<div class="input-group-addon {% if not form.vars.valid %} has-error{% endif %}"> <div class="modal-body">
<i class="{{ 'search'|icon }}"></i> {{ form(form) }}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
</div> </div>
{{ form_widget(form.searchTerm, {'attr': {'placeholder': 'search'|trans, 'autocomplete': 'off', 'data-toggle': 'dropdown', 'class': searchTermClass, 'aria-haspopup': 'true', 'aria-expanded': 'true'}}) }}
<ul class="dropdown-menu pre-scrollable" role="menu" aria-labelledby="searchTerm">
<li>
<div class="container-fluid">
{% if not form.searchTerm.vars.valid %}
{{ form_errors(form.searchTerm) }}
<br>
{% endif %}
{{ form_widget(form) }}
<div class="form-group">
<div class="col-sm-3 col-xs-4"></div>
<div class="col-sm-9 col-xs-8">
{% if not theme_config('auto_reload_datatable') %}
{# FIXME this should trigger the datatable reload and not "really" submit the form #}
<input type="submit" value="{{ 'search'|trans }}" class="btn btn-primary" />
{% endif %}
<a href="#" class="search-cancel pull-right">{{ 'action.close'|trans }}</a>
</div>
</div>
</div>
</li>
</ul>
</div> </div>
{{ form_end(form) }}
</div> </div>
</div> </div>
{{ form_end(form) }}
{% endmacro %} {% endmacro %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% import "project/actions.html.twig" as actions %} {% import "project/actions.html.twig" as actions %}
{% set columns = { {% set columns = {
@@ -10,8 +10,8 @@
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans}, 'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
'orderNumber': {'class': 'hidden-xs hidden-sm hidden'}, 'orderNumber': {'class': 'hidden-xs hidden-sm hidden'},
'orderDate': {'class': 'hidden-xs hidden-sm hidden'}, 'orderDate': {'class': 'hidden-xs hidden-sm hidden'},
'start': {'class': 'hidden-xs hidden-sm hidden', 'title': 'label.project_start'|trans}, 'project_start': {'class': 'hidden-xs hidden-sm hidden'},
'end': {'class': 'hidden-xs hidden-sm hidden', 'title': 'label.project_end'|trans}, 'project_end': {'class': 'hidden-xs hidden-sm hidden'},
} %} } %}
{% for field in metaColumns %} {% for field in metaColumns %}
{% set columns = columns|merge({ {% set columns = columns|merge({
@@ -29,7 +29,7 @@
{% set tableName = 'project_admin' %} {% set tableName = 'project_admin' %}
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.projects('index') }}{% endblock %} {% block page_actions %}{{ actions.projects('index') }}{% endblock %}
{% block main_before %} {% block main_before %}
@@ -50,8 +50,8 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'end') }}">{% if entry.end is not null %}{{ entry.end|date_full }}{% endif %}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'project_end') }}">{% if entry.end is not null %}{{ entry.end|date_full }}{% endif %}</td>
{% for field in metaColumns %} {% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }} {{ tables.datatable_meta_column(entry, field) }}

View File

@@ -1,11 +1,11 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "tags/actions.html.twig" as actions %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% import "tags/actions.html.twig" as actions %}
{% block page_title %}{{ 'menu.tags'|trans }}{% endblock %} {% block page_title %}{{ 'menu.tags'|trans }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.tags('index') }}{% endblock %} {% block page_actions %}{{ actions.tags('index') }}{% endblock %}
{% block main %} {% block main %}

View File

@@ -1,11 +1,11 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "team/actions.html.twig" as actions %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% import "team/actions.html.twig" as actions %}
{% block page_title %}{{ 'teams.title'|trans({}, 'teams') }}{% endblock %} {% block page_title %}{{ 'teams.title'|trans({}, 'teams') }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.teams('index') }}{% endblock %} {% block page_actions %}{{ actions.teams('index') }}{% endblock %}
{% block main %} {% block main %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/search.html.twig" as search %}
{% if not showSummary is defined %} {% if not showSummary is defined %}
{% set showSummary = false %} {% set showSummary = false %}
@@ -48,7 +48,7 @@
'actions': {'class': 'actions alwaysVisible'}, 'actions': {'class': 'actions alwaysVisible'},
}) %} }) %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block main_before %} {% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }} {{ tables.data_table_column_modal(tableName, columns) }}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/search.html.twig" as search %}
{% import "user/actions.html.twig" as actions %} {% import "user/actions.html.twig" as actions %}
{% set columns = { {% set columns = {
@@ -26,7 +26,7 @@
{% set tableName = 'user_admin' %} {% set tableName = 'user_admin' %}
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %} {% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.users('index') }}{% endblock %} {% block page_actions %}{{ actions.users('index') }}{% endblock %}
{% block page_icon %}{{ 'user'|icon }}{% endblock %} {% block page_icon %}{{ 'user'|icon }}{% endblock %}

View File

@@ -182,10 +182,12 @@ class InvoiceCreateCommandTest extends KernelTestCase
$commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--set-exported' => null, '--customer' => 1, '--template' => 'Invoice', '--start' => '2020-01-01', '--end' => '2020-03-01']); $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--set-exported' => null, '--customer' => 1, '--template' => 'Invoice', '--start' => '2020-01-01', '--end' => '2020-03-01']);
$output = $commandTester->getDisplay(); $output = $commandTester->getDisplay();
$this->assertStringContainsString('+----+----------+-------+------------- Created 1 invoice(s) --------------------------------------+', $output); $this->assertStringContainsString('Created 1 invoice(s)', $output);
$this->assertStringContainsString('| ID | Customer | Total | Filename |', $output); $this->assertStringContainsString('| ID', $output);
$this->assertStringContainsString('+----+----------+-------+-------------------------------------------------------------------------+', $output); $this->assertStringContainsString('| Customer', $output);
$this->assertStringContainsString('| Test | 0 EUR | /', $output); $this->assertStringContainsString('| Total', $output);
$this->assertStringContainsString('| Filename', $output);
$this->assertStringContainsString('0 EUR', $output);
$this->assertStringContainsString('/tests/_data/invoices/' . ((new \DateTime())->format('Y')) . '-001-Test.html |', $output); $this->assertStringContainsString('/tests/_data/invoices/' . ((new \DateTime())->format('Y')) . '-001-Test.html |', $output);
} }

View File

@@ -47,7 +47,6 @@ class ThemeConfigurationTest extends TestCase
'company' => null, 'company' => null,
'title' => null, 'title' => null,
], ],
'auto_reload_datatable' => false,
'tags_create' => true, 'tags_create' => true,
]; ];
} }
@@ -61,7 +60,6 @@ class ThemeConfigurationTest extends TestCase
public function testConfigs() public function testConfigs()
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertFalse($sut->isAutoReloadDatatable());
$this->assertTrue($sut->isAllowTagCreation()); $this->assertTrue($sut->isAllowTagCreation());
$this->assertNull($sut->getTitle()); $this->assertNull($sut->getTitle());
} }
@@ -73,5 +71,6 @@ class ThemeConfigurationTest extends TestCase
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
$this->assertEquals('', $sut->getSelectPicker()); $this->assertEquals('', $sut->getSelectPicker());
$this->assertFalse($sut->isAutoReloadDatatable());
} }
} }

View File

@@ -59,7 +59,7 @@ class ActivityControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/activity/'); $this->assertAccessIsGranted($client, '/admin/activity/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',
'visibility' => 1, 'visibility' => 1,
@@ -102,7 +102,7 @@ class ActivityControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/activity/'); $this->assertAccessIsGranted($client, '/admin/activity/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/activity/export')); $form->getFormNode()->setAttribute('action', $this->createUrl('/admin/activity/export'));
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',

View File

@@ -60,7 +60,7 @@ class CustomerControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/customer/'); $this->assertAccessIsGranted($client, '/admin/customer/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',
'visibility' => 1, 'visibility' => 1,
@@ -92,7 +92,7 @@ class CustomerControllerTest extends ControllerBaseTest
$this->request($client, '/admin/customer/'); $this->request($client, '/admin/customer/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/customer/export')); $form->getFormNode()->setAttribute('action', $this->createUrl('/admin/customer/export'));
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',

View File

@@ -66,7 +66,7 @@ class ProjectControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/project/'); $this->assertAccessIsGranted($client, '/admin/project/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',
'visibility' => 1, 'visibility' => 1,
@@ -108,7 +108,7 @@ class ProjectControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/project/'); $this->assertAccessIsGranted($client, '/admin/project/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/project/export')); $form->getFormNode()->setAttribute('action', $this->createUrl('/admin/project/export'));
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'feature:timetracking foo', 'searchTerm' => 'feature:timetracking foo',

View File

@@ -55,7 +55,7 @@ class TagControllerTest extends ControllerBaseTest
$this->request($client, '/admin/tags/'); $this->request($client, '/admin/tags/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'Support', 'searchTerm' => 'Support',
]); ]);

View File

@@ -41,7 +41,7 @@ class TeamControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/teams/'); $this->assertAccessIsGranted($client, '/admin/teams/');
$this->assertPageActions($client, [ $this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#', 'search' => '#',
'create' => $this->createUrl('/admin/teams/create'), 'create' => $this->createUrl('/admin/teams/create'),
'help' => 'https://www.kimai.org/documentation/teams.html' 'help' => 'https://www.kimai.org/documentation/teams.html'
]); ]);
@@ -62,7 +62,7 @@ class TeamControllerTest extends ControllerBaseTest
$this->assertAccessIsGranted($client, '/admin/teams/'); $this->assertAccessIsGranted($client, '/admin/teams/');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'foo', 'searchTerm' => 'foo',
]); ]);

View File

@@ -39,7 +39,7 @@ class TimesheetControllerTest extends ControllerBaseTest
// there are no records by default in the test database // there are no records by default in the test database
$this->assertHasNoEntriesWithFilter($client); $this->assertHasNoEntriesWithFilter($client);
$this->assertPageActions($client, [ $this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#', 'search' => '#',
'toolbar-action exporter-csv' => $this->createUrl('/timesheet/export/csv'), 'toolbar-action exporter-csv' => $this->createUrl('/timesheet/export/csv'),
'toolbar-action exporter-print' => $this->createUrl('/timesheet/export/print'), 'toolbar-action exporter-print' => $this->createUrl('/timesheet/export/print'),
'toolbar-action exporter-pdf' => $this->createUrl('/timesheet/export/pdf'), 'toolbar-action exporter-pdf' => $this->createUrl('/timesheet/export/pdf'),
@@ -67,7 +67,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d'); $dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'state' => 1, 'state' => 1,
'pageSize' => 25, 'pageSize' => 25,
@@ -113,7 +113,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d'); $dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'location:homeoffice foobar', 'searchTerm' => 'location:homeoffice foobar',
]); ]);
@@ -138,7 +138,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d'); $dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/timesheet/export/print')); $form->getFormNode()->setAttribute('action', $this->createUrl('/timesheet/export/print'));
$client->submit($form, [ $client->submit($form, [
'state' => 1, 'state' => 1,

View File

@@ -41,7 +41,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$this->assertHasNoEntriesWithFilter($client); $this->assertHasNoEntriesWithFilter($client);
$this->assertPageActions($client, [ $this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#', 'search' => '#',
'toolbar-action exporter-csv' => $this->createUrl('/team/timesheet/export/csv'), 'toolbar-action exporter-csv' => $this->createUrl('/team/timesheet/export/csv'),
'toolbar-action exporter-print' => $this->createUrl('/team/timesheet/export/print'), 'toolbar-action exporter-print' => $this->createUrl('/team/timesheet/export/print'),
'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'), 'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'),
@@ -73,7 +73,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d'); $dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'state' => 1, 'state' => 1,
'users' => [$user->getId()], 'users' => [$user->getId()],
@@ -119,7 +119,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d'); $dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'location:homeoffice foobar', 'searchTerm' => 'location:homeoffice foobar',
]); ]);
@@ -150,7 +150,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d'); $dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export/print')); $form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export/print'));
$client->submit($form, [ $client->submit($form, [
'state' => 1, 'state' => 1,

View File

@@ -35,7 +35,7 @@ class UserControllerTest extends ControllerBaseTest
$this->assertHasDataTable($client); $this->assertHasDataTable($client);
$this->assertDataTableRowCount($client, 'datatable_user_admin', 7); $this->assertDataTableRowCount($client, 'datatable_user_admin', 7);
$this->assertPageActions($client, [ $this->assertPageActions($client, [
'search search-toggle visible-xs-inline' => '#', 'search' => '#',
'visibility' => '#', 'visibility' => '#',
'download toolbar-action' => $this->createUrl('/admin/user/export'), 'download toolbar-action' => $this->createUrl('/admin/user/export'),
'create' => $this->createUrl('/admin/user/create'), 'create' => $this->createUrl('/admin/user/create'),
@@ -50,7 +50,7 @@ class UserControllerTest extends ControllerBaseTest
$this->request($client, '/admin/user/'); $this->request($client, '/admin/user/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'hourly_rate:35 tony', 'searchTerm' => 'hourly_rate:35 tony',
'role' => 'ROLE_TEAMLEAD', 'role' => 'ROLE_TEAMLEAD',
@@ -83,7 +83,7 @@ class UserControllerTest extends ControllerBaseTest
$this->request($client, '/admin/user/'); $this->request($client, '/admin/user/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.header-search')->form(); $form = $client->getCrawler()->filter('form.searchform')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/user/export')); $form->getFormNode()->setAttribute('action', $this->createUrl('/admin/user/export'));
$client->submit($form, [ $client->submit($form, [
'searchTerm' => 'hourly_rate:35 tony', 'searchTerm' => 'hourly_rate:35 tony',

View File

@@ -169,7 +169,6 @@ class AppExtensionTest extends TestCase
'title' => null, 'title' => null,
'translation' => null, 'translation' => null,
], ],
'auto_reload_datatable' => false,
'autocomplete_chars' => 3, 'autocomplete_chars' => 3,
'tags_create' => true, 'tags_create' => true,
'calendar' => [ 'calendar' => [

View File

@@ -343,7 +343,6 @@ class ConfigurationTest extends TestCase
'active_warning' => 3, 'active_warning' => 3,
'box_color' => 'blue', 'box_color' => 'blue',
'select_type' => 'selectpicker', 'select_type' => 'selectpicker',
'auto_reload_datatable' => false,
'show_about' => true, 'show_about' => true,
'chart' => [ 'chart' => [
'background_color' => '#3c8dbc', 'background_color' => '#3c8dbc',

View File

@@ -0,0 +1,52 @@
<?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\Entity;
use App\Entity\Bookmark;
use App\Entity\User;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Entity\Bookmark
*/
class BookmarkTest extends TestCase
{
public function testDefaultValues()
{
$sut = new Bookmark();
$this->assertNull($sut->getId());
$this->assertNull($sut->getName());
$this->assertIsArray($sut->getContent());
$this->assertEquals([], $sut->getContent());
$this->assertNull($sut->getType());
$this->assertNull($sut->getUser());
}
public function testSetterAndGetter()
{
$sut = new Bookmark();
$sut->setName('foo-bar');
$this->assertEquals('foo-bar', $sut->getName());
$sut->setContent(['test' => 'foo-bar', 'hello' => 'world']);
$this->assertEquals(['test' => 'foo-bar', 'hello' => 'world'], $sut->getContent());
$sut->setType('sdsdsd');
$this->assertEquals('sdsdsd', $sut->getType());
$user = new User();
$sut->setUser($user);
$this->assertEquals($user, $sut->getUser());
$sut2 = clone $sut;
$this->assertEquals('sdsdsd', $sut2->getType());
$this->assertEquals('foo-bar', $sut2->getName());
}
}

View File

@@ -95,7 +95,7 @@ class PageActionsEventTest extends TestCase
$this->assertEquals(8, $sut->countActions()); $this->assertEquals(8, $sut->countActions());
$expected = [ $expected = [
'search' => ['class' => 'search-toggle visible-xs-inline'], 'search' => ['modal' => '#modal_search'],
'divider0' => null, 'divider0' => null,
'back' => ['url' => 'foo1', 'translation_domain' => 'actions'], 'back' => ['url' => 'foo1', 'translation_domain' => 'actions'],
'visibility' => ['modal' => '#foo2'], 'visibility' => ['modal' => '#foo2'],

View 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\Form\Model;
use App\Form\Model\DateRange;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Form\Model\DateRange
*/
class DateRangeTest extends TestCase
{
public function testDefaultValues()
{
$sut = new DateRange();
self::assertNull($sut->getBegin());
self::assertNull($sut->getEnd());
}
public function testSetterAndGetter()
{
$begin = new \DateTime('now');
$end = new \DateTime('2018-11-25 18:45:32');
$sut = new DateRange();
self::assertInstanceOf(DateRange::class, $sut->setBegin($begin));
self::assertInstanceOf(DateRange::class, $sut->setEnd($end));
self::assertEquals($begin->format('Y-m-d') . ' 00:00:00', $sut->getBegin()->format('Y-m-d H:i:s'));
self::assertEquals('2018-11-25 23:59:59', $sut->getEnd()->format('Y-m-d H:i:s'));
}
}

View File

@@ -61,15 +61,22 @@ class ConfigExtensionTest extends TestCase
'company' => null, 'company' => null,
'title' => null, 'title' => null,
], ],
'auto_reload_datatable' => false,
]; ];
} }
public function testPrefix() public function testPrefix()
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);
self::assertFalse($sut->getThemeConfig('auto_reload_datatable'));
self::assertEquals(3, $sut->getThemeConfig('active_warning')); self::assertEquals(3, $sut->getThemeConfig('active_warning'));
self::assertEquals('green', $sut->getThemeConfig('box_color')); self::assertEquals('green', $sut->getThemeConfig('box_color'));
} }
/**
* @group legacy
*/
public function testDeprecation()
{
$sut = $this->getSut($this->getDefaultSettings(), []);
self::assertFalse($sut->getThemeConfig('auto_reload_datatable'));
}
} }

View File

@@ -60,6 +60,26 @@
<source>search</source> <source>search</source>
<target>Suchen</target> <target>Suchen</target>
</trans-unit> </trans-unit>
<trans-unit id="label.searchTerm">
<source>label.searchTerm</source>
<target>Suchbegriff</target>
</trans-unit>
<trans-unit id="label.set_as_default">
<source>label.set_as_default</source>
<target>Als Standard-Einstellung speichern</target>
</trans-unit>
<trans-unit id="label.asc">
<source>label.asc</source>
<target>Aufsteigend</target>
</trans-unit>
<trans-unit id="label.desc">
<source>label.desc</source>
<target>Absteigend</target>
</trans-unit>
<trans-unit id="label.orderBy">
<source>label.orderBy</source>
<target>Sortieren nach</target>
</trans-unit>
<trans-unit id="my.profile"> <trans-unit id="my.profile">
<source>my.profile</source> <source>my.profile</source>
<target>Mein Profil</target> <target>Mein Profil</target>

View File

@@ -60,6 +60,26 @@
<source>search</source> <source>search</source>
<target>Search</target> <target>Search</target>
</trans-unit> </trans-unit>
<trans-unit id="label.searchTerm">
<source>label.searchTerm</source>
<target>Search term</target>
</trans-unit>
<trans-unit id="label.set_as_default">
<source>label.set_as_default</source>
<target>Save as default setting</target>
</trans-unit>
<trans-unit id="label.asc">
<source>label.asc</source>
<target>Ascending</target>
</trans-unit>
<trans-unit id="label.desc">
<source>label.desc</source>
<target>Descending</target>
</trans-unit>
<trans-unit id="label.orderBy">
<source>label.orderBy</source>
<target>Order by</target>
</trans-unit>
<trans-unit id="my.profile"> <trans-unit id="my.profile">
<source>my.profile</source> <source>my.profile</source>
<target>My profile</target> <target>My profile</target>