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:
@@ -23,7 +23,9 @@ class ThemeConfiguration implements SystemBundleConfiguration, \ArrayAccess
|
||||
|
||||
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
|
||||
|
||||
@@ -10,11 +10,16 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\User;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\LocaleFormats;
|
||||
use Psr\Log\LoggerInterface;
|
||||
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\Contracts\Service\ServiceSubscriberInterface;
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Exception;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -77,14 +76,10 @@ final class ActivityController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
|
||||
@@ -76,11 +76,8 @@ final class CustomerController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
@@ -88,14 +88,10 @@ final class ProjectController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('project/index.html.twig', [
|
||||
|
||||
@@ -429,14 +429,6 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setLabel('theme.tags_create')
|
||||
->setType(CheckboxType::class)
|
||||
->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())
|
||||
->setSection(SystemConfigurationModel::SECTION_CALENDAR)
|
||||
|
||||
@@ -43,11 +43,8 @@ class TagController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
$tags = $repository->getTagCount($query);
|
||||
|
||||
@@ -55,11 +55,8 @@ final class TeamController extends AbstractController
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_team');
|
||||
}
|
||||
|
||||
$teams = $repository->getPagerfantaForQuery($query);
|
||||
|
||||
@@ -77,24 +77,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
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->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
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);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute($route);
|
||||
}
|
||||
|
||||
$tags = $query->getTags(true);
|
||||
@@ -486,11 +473,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(TimesheetQuery $query)
|
||||
protected function getToolbarForm(TimesheetQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TimesheetToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl($this->getTimesheetRoute(), [
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -36,7 +37,11 @@ class TimesheetController extends TimesheetAbstractController
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,11 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,6 @@ use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -78,14 +77,10 @@ final class UserController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_user');
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::USERS);
|
||||
|
||||
@@ -404,9 +404,6 @@ class Configuration implements ConfigurationInterface
|
||||
->defaultValue('selectpicker')
|
||||
->setDeprecated()
|
||||
->end()
|
||||
->booleanNode('auto_reload_datatable')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->booleanNode('tags_create')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
|
||||
120
src/Entity/Bookmark.php
Normal file
120
src/Entity/Bookmark.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
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
|
||||
|
||||
@@ -13,13 +13,7 @@ use DateTime;
|
||||
|
||||
final class DateRange
|
||||
{
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $begin;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $end;
|
||||
|
||||
public function getBegin(): ?DateTime
|
||||
@@ -30,6 +24,7 @@ final class DateRange
|
||||
public function setBegin(DateTime $begin): DateRange
|
||||
{
|
||||
$this->begin = $begin;
|
||||
$this->begin->setTime(0, 0, 0);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -42,6 +37,7 @@ final class DateRange
|
||||
public function setEnd(DateTime $end): DateRange
|
||||
{
|
||||
$this->end = $end;
|
||||
$this->end->setTime(23, 59, 59);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -300,6 +300,8 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
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, [
|
||||
'constraints' => [
|
||||
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)
|
||||
{
|
||||
@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, [
|
||||
'constraints' => [
|
||||
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)
|
||||
{
|
||||
$builder->add('tags', TagsType::class, [
|
||||
|
||||
@@ -45,8 +45,8 @@ class ActivityToolbarForm extends AbstractToolbarForm
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, ActivityQuery::ACTIVITY_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, ActivityQuery::ACTIVITY_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,8 +27,8 @@ class CustomerToolbarForm extends AbstractToolbarForm
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, CustomerQuery::CUSTOMER_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, CustomerQuery::CUSTOMER_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,8 +28,8 @@ class ProjectToolbarForm extends AbstractToolbarForm
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, ProjectQuery::PROJECT_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, ProjectQuery::PROJECT_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,8 @@ class TagToolbarForm extends AbstractToolbarForm
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, TagQuery::TAG_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, TagQuery::TAG_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,8 +24,8 @@ class TeamToolbarForm extends AbstractToolbarForm
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, TeamQuery::TEAM_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, TeamQuery::TEAM_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,8 +41,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,8 +28,8 @@ class UserToolbarForm extends AbstractToolbarForm
|
||||
$this->addVisibilityChoice($builder, 'label.active');
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
$this->addHiddenOrder($builder);
|
||||
$this->addHiddenOrderBy($builder, UserQuery::USER_ORDER_ALLOWED);
|
||||
$this->addOrder($builder);
|
||||
$this->addOrderBy($builder, UserQuery::USER_ORDER_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,13 +27,28 @@ class PageSizeType extends AbstractType
|
||||
'label' => 'label.pageSize',
|
||||
'choices' => [
|
||||
10 => 10,
|
||||
15 => 15,
|
||||
20 => 20,
|
||||
25 => 25,
|
||||
30 => 30,
|
||||
35 => 35,
|
||||
40 => 40,
|
||||
45 => 45,
|
||||
50 => 50,
|
||||
75 => 75,
|
||||
60 => 60,
|
||||
70 => 70,
|
||||
80 => 80,
|
||||
90 => 90,
|
||||
100 => 100,
|
||||
125 => 125,
|
||||
150 => 150,
|
||||
175 => 175,
|
||||
200 => 200,
|
||||
250 => 250,
|
||||
300 => 300,
|
||||
350 => 350,
|
||||
400 => 400,
|
||||
450 => 450,
|
||||
500 => 500
|
||||
],
|
||||
'placeholder' => null,
|
||||
|
||||
@@ -33,7 +33,7 @@ class SearchTermType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'search',
|
||||
'label' => 'label.searchTerm',
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Length(['min' => 3, 'allowEmptyString' => true])
|
||||
|
||||
46
src/Migrations/Version20210316224358.php
Normal file
46
src/Migrations/Version20210316224358.php
Normal 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');
|
||||
}
|
||||
}
|
||||
52
src/Repository/BookmarkRepository.php
Normal file
52
src/Repository/BookmarkRepository.php
Normal 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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,16 @@ class CustomerRepository extends EntityRepository
|
||||
->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()) {
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
|
||||
|
||||
@@ -271,6 +271,12 @@ class ProjectRepository extends EntityRepository
|
||||
case 'customer':
|
||||
$orderBy = 'c.name';
|
||||
break;
|
||||
case 'project_start':
|
||||
$orderBy = 'p.start';
|
||||
break;
|
||||
case 'project_end':
|
||||
$orderBy = 'p.end';
|
||||
break;
|
||||
default:
|
||||
$orderBy = 'p.' . $orderBy;
|
||||
break;
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Entity\Project;
|
||||
*/
|
||||
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[]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Utils\SearchTerm;
|
||||
@@ -78,6 +79,14 @@ class BaseQuery
|
||||
* @var SearchTerm|null
|
||||
*/
|
||||
private $searchTerm;
|
||||
/**
|
||||
* @var Bookmark
|
||||
*/
|
||||
private $bookmark;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @param Team[] $teams
|
||||
@@ -274,6 +283,32 @@ class BaseQuery
|
||||
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
|
||||
{
|
||||
$query->setDefaults($this->defaults);
|
||||
|
||||
@@ -18,7 +18,7 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
|
||||
public const CUSTOMER_ORDER_ALLOWED = [
|
||||
'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()
|
||||
|
||||
@@ -19,8 +19,7 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
use VisibilityTrait;
|
||||
|
||||
public const PROJECT_ORDER_ALLOWED = [
|
||||
'id', 'name', 'comment', 'customer', 'orderNumber', 'projectStart',
|
||||
'projectEnd', 'orderNumber', 'orderDate', 'start', 'end', 'visible'
|
||||
'id', 'name', 'comment', 'customer', 'orderNumber', 'orderDate', 'project_start', 'project_end', 'budget', 'timeBudget', 'visible'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,11 @@ final class ConfigExtension extends AbstractExtension
|
||||
public function getThemeConfig(string $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':
|
||||
return $this->configuration->getTimesheetActiveEntriesSoftLimit();
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ final class IconExtension extends AbstractExtension
|
||||
'avatar' => 'fas fa-user',
|
||||
'back' => 'fas fa-long-arrow-alt-left',
|
||||
'barcode' => 'fas fa-barcode',
|
||||
'bookmark' => 'far fa-star',
|
||||
'bookmarked' => 'fas fa-star',
|
||||
'calendar' => 'far fa-calendar-alt',
|
||||
'clock' => 'far fa-clock',
|
||||
'comment' => 'far fa-comment',
|
||||
|
||||
Reference in New Issue
Block a user