refactored search with free search term support (#1064)
This commit is contained in:
@@ -18,6 +18,11 @@ class ThemeConfiguration implements SystemBundleConfiguration
|
||||
return 'theme';
|
||||
}
|
||||
|
||||
public function isAutoReloadDatatable(): bool
|
||||
{
|
||||
return (bool) $this->find('auto_reload_datatable');
|
||||
}
|
||||
|
||||
public function getSelectPicker(): string
|
||||
{
|
||||
return (string) $this->find('select_type');
|
||||
|
||||
@@ -266,6 +266,14 @@ class SystemConfigurationController extends AbstractController
|
||||
->setLabel('theme.markdown_content')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// FIXME should that be configurable per user?
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('theme.auto_reload_datatable')
|
||||
->setLabel('theme.auto_reload_datatable') // FIXME translation
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
*/
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_CALENDAR)
|
||||
|
||||
@@ -314,6 +314,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('select_type')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->scalarNode('auto_reload_datatable')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->booleanNode('show_about')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
|
||||
48
src/Form/DataTransformer/SearchTermTransformer.php
Normal file
48
src/Form/DataTransformer/SearchTermTransformer.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Form\DataTransformer;
|
||||
|
||||
use App\Utils\SearchTerm;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
class SearchTermTransformer implements DataTransformerInterface
|
||||
{
|
||||
/**
|
||||
* Transforms a SearchTerm object to a string.
|
||||
*
|
||||
* @param SearchTerm|null $searchTerm
|
||||
* @return string
|
||||
*/
|
||||
public function transform($searchTerm)
|
||||
{
|
||||
if (empty($searchTerm) || !$searchTerm instanceof SearchTerm) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $searchTerm->getOriginalSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a string to a SearchTerm object.
|
||||
*
|
||||
* @param string $searchTerm
|
||||
* @return SearchTerm|null
|
||||
* @throws TransformationFailedException if object (issue) is not found
|
||||
*/
|
||||
public function reverseTransform($searchTerm)
|
||||
{
|
||||
if (empty($searchTerm)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SearchTerm($searchTerm);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Converts normal select boxes into javascript enhanced versions.
|
||||
@@ -49,6 +50,10 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($options['selectpicker']) && false === $options['selectpicker']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($view->vars['attr'])) {
|
||||
$view->vars['attr'] = [];
|
||||
}
|
||||
@@ -58,4 +63,14 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
['class' => 'selectpicker', 'data-live-search' => true, 'data-width' => '100%']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefined(['selectpicker']);
|
||||
$resolver->setAllowedTypes('selectpicker', 'boolean');
|
||||
$resolver->setDefault('selectpicker', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Form\Type\PageSizeType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\SearchTermType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Form\Type\UserType;
|
||||
@@ -50,11 +51,17 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function getSelectpickerConfig(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function addUserChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
'label' => 'label.user',
|
||||
'required' => false,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -64,6 +71,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'label' => 'label.user',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,6 +96,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
@@ -98,7 +107,8 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'label' => $label
|
||||
'label' => $label,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -106,6 +116,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
'required' => false,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -113,6 +124,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
$builder->add('role', UserRoleType::class, [
|
||||
'required' => false,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -149,6 +161,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
@@ -177,6 +190,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
@@ -196,6 +210,11 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addSearchTermInputField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('searchTerm', SearchTermType::class);
|
||||
}
|
||||
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
@@ -207,6 +226,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -221,6 +241,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||
],
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ class ActivityToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$builder->add('globalsOnly', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'yes' => 1,
|
||||
@@ -34,9 +35,10 @@ class ActivityToolbarForm extends AbstractToolbarForm
|
||||
'placeholder' => null,
|
||||
'required' => false,
|
||||
'label' => 'label.globalsOnly',
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ class CustomerToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class ExportToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
protected function getSelectpickerConfig(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
|
||||
@@ -21,11 +21,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
protected function getSelectpickerConfig(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
@@ -52,6 +58,7 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
$builder->add('template', InvoiceTemplateType::class, [
|
||||
'required' => true,
|
||||
'placeholder' => null,
|
||||
'selectpicker' => $this->getSelectpickerConfig(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ class ProjectToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class TagToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class TeamToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addSearchTermInputField($builder);
|
||||
if ($options['include_user']) {
|
||||
$this->addUserChoice($builder);
|
||||
}
|
||||
@@ -33,6 +32,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ class UserToolbarForm extends AbstractToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder, 'label.active');
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addUserRoleChoice($builder);
|
||||
$this->addVisibilityChoice($builder, 'label.active');
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
51
src/Form/Type/SearchTermType.php
Normal file
51
src/Form/Type/SearchTermType.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use App\Form\DataTransformer\SearchTermTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
class SearchTermType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addModelTransformer(new SearchTermTransformer());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'search',
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Length(['min' => 3])
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
}
|
||||
@@ -298,6 +298,37 @@ class ActivityRepository extends EntityRepository
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$qb->leftJoin('a.meta', 'meta');
|
||||
$searchAnd->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('meta.name', ':metaName'),
|
||||
$qb->expr()->like('meta.value', ':metaValue')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('metaName', $metaName);
|
||||
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||
}
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('a.name', ':searchTerm'),
|
||||
$qb->expr()->like('a.comment', ':searchTerm')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,6 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
$qb->select('c')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.meta', 'meta')
|
||||
->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
if (CustomerQuery::SHOW_VISIBLE == $query->getVisibility()) {
|
||||
@@ -223,6 +222,42 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$qb->leftJoin('c.meta', 'meta');
|
||||
$searchAnd->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('meta.name', ':metaName'),
|
||||
$qb->expr()->like('meta.value', ':metaValue')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('metaName', $metaName);
|
||||
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||
}
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('c.name', ':searchTerm'),
|
||||
$qb->expr()->like('c.comment', ':searchTerm'),
|
||||
$qb->expr()->like('c.number', ':searchTerm'),
|
||||
$qb->expr()->like('c.contact', ':searchTerm'),
|
||||
$qb->expr()->like('c.phone', ':searchTerm'),
|
||||
$qb->expr()->like('c.email', ':searchTerm'),
|
||||
$qb->expr()->like('c.address', ':searchTerm')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ class ProjectRepository extends EntityRepository
|
||||
->select('p')
|
||||
->from(Project::class, 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->addOrderBy('p.' . $query->getOrderBy(), $query->getOrder())
|
||||
;
|
||||
|
||||
if (in_array($query->getVisibility(), [ProjectQuery::SHOW_VISIBLE, ProjectQuery::SHOW_HIDDEN])) {
|
||||
@@ -236,7 +237,37 @@ class ProjectRepository extends EntityRepository
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
$qb->orderBy('p.' . $query->getOrderBy(), $query->getOrder());
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$qb->leftJoin('p.meta', 'meta');
|
||||
$searchAnd->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('meta.name', ':metaName'),
|
||||
$qb->expr()->like('meta.value', ':metaValue')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('metaName', $metaName);
|
||||
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||
}
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('p.name', ':searchTerm'),
|
||||
$qb->expr()->like('p.comment', ':searchTerm'),
|
||||
$qb->expr()->like('p.orderNumber', ':searchTerm')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Utils\SearchTerm;
|
||||
|
||||
/**
|
||||
* Base class for advanced Repository queries.
|
||||
@@ -55,6 +56,10 @@ class BaseQuery
|
||||
* @var Team[]
|
||||
*/
|
||||
private $teams = [];
|
||||
/**
|
||||
* @var SearchTerm|null
|
||||
*/
|
||||
private $searchTerm;
|
||||
|
||||
public function addTeam(Team $team): self
|
||||
{
|
||||
@@ -188,6 +193,27 @@ class BaseQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSearchTerm(): bool
|
||||
{
|
||||
return null !== $this->searchTerm;
|
||||
}
|
||||
|
||||
public function getSearchTerm(): ?SearchTerm
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SearchTerm|null $searchTerm
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function setSearchTerm(?SearchTerm $searchTerm)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the query has changed fields, compared to the original state.
|
||||
*
|
||||
@@ -203,6 +229,14 @@ class BaseQuery
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($this->teams)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (null !== $this->searchTerm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,26 @@ class TagRepository extends EntityRepository
|
||||
->leftJoin('tag.timesheets', 'timesheets')
|
||||
->addGroupBy('tag.id')
|
||||
->addGroupBy('tag.name')
|
||||
->orderBy('tag.' . $query->getOrderBy(), $query->getOrder());
|
||||
->addOrderBy('tag.' . $query->getOrderBy(), $query->getOrder())
|
||||
;
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('tag.name', ':searchTerm')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($qb->getQuery(), false));
|
||||
$paginator->setMaxPerPage($query->getPageSize());
|
||||
|
||||
@@ -104,9 +104,17 @@ class TeamRepository extends EntityRepository
|
||||
$qb
|
||||
->select('t')
|
||||
->from(Team::class, 't')
|
||||
->addOrderBy('t.' . $query->getOrderBy(), $query->getOrder())
|
||||
;
|
||||
|
||||
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
if (!empty($query->getSearchTerm())) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('t.name', ':likeContains')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('likeContains', '%' . $query->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -551,6 +551,7 @@ class TimesheetRepository extends EntityRepository
|
||||
->select('t')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.project', 'p')
|
||||
->addOrderBy('t.' . $query->getOrderBy(), $query->getOrder())
|
||||
;
|
||||
|
||||
$user = [];
|
||||
@@ -636,7 +637,33 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
|
||||
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$qb->leftJoin('t.meta', 'meta');
|
||||
$searchAnd->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('meta.name', ':metaName'),
|
||||
$qb->expr()->like('meta.value', ':metaValue')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('metaName', $metaName);
|
||||
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||
}
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->like('t.description', ':searchTerm')
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,39 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
$qb->andWhere($rolesWhere);
|
||||
}
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$searchTerm = $query->getSearchTerm();
|
||||
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$qb->leftJoin('u.preferences', 'meta');
|
||||
$searchAnd->add(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('meta.name', ':metaName'),
|
||||
$qb->expr()->like('meta.value', ':metaValue')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('metaName', $metaName);
|
||||
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||
}
|
||||
|
||||
if ($searchTerm->hasSearchTerm()) {
|
||||
$searchAnd->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->like('u.alias', ':searchTerm'),
|
||||
$qb->expr()->like('u.title', ':searchTerm'),
|
||||
$qb->expr()->like('u.email', ':searchTerm'),
|
||||
$qb->expr()->like('u.username', ':searchTerm')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||
}
|
||||
|
||||
if ($searchAnd->count() > 0) {
|
||||
$qb->andWhere($searchAnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
|
||||
|
||||
46
src/Twig/ConfigExtension.php
Normal file
46
src/Twig/ConfigExtension.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Twig;
|
||||
|
||||
use App\Configuration\ThemeConfiguration;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class ConfigExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var ThemeConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ThemeConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('theme_config', [$this, 'getThemeConfig']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getThemeConfig(string $name)
|
||||
{
|
||||
return $this->configuration->find($name);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ final class IconExtension extends AbstractExtension
|
||||
'print' => 'fas fa-print',
|
||||
'project' => 'fas fa-briefcase',
|
||||
'repeat' => 'fas fa-redo-alt',
|
||||
'search' => 'fas fa-search',
|
||||
'start' => 'fas fa-play-circle',
|
||||
'start-small' => 'far fa-play-circle',
|
||||
'stop' => 'fas fa-stop',
|
||||
|
||||
@@ -25,9 +25,6 @@ class TitleExtension extends AbstractExtension
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator, ThemeConfiguration $configuration)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
|
||||
90
src/Utils/SearchTerm.php
Normal file
90
src/Utils/SearchTerm.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Utils;
|
||||
|
||||
final class SearchTerm
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $originalTerm;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $term;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(string $searchTerm)
|
||||
{
|
||||
$this->originalTerm = $searchTerm;
|
||||
$this->parse($searchTerm);
|
||||
}
|
||||
|
||||
private function parse(string $searchTerm)
|
||||
{
|
||||
$terms = explode(' ', $searchTerm);
|
||||
$fields = [];
|
||||
$finalTerm = [];
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$tmp = explode(':', $term);
|
||||
if (count($tmp) === 2) {
|
||||
$fields[$tmp[0]] = $tmp[1];
|
||||
} else {
|
||||
$finalTerm[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$this->term = implode(' ', $finalTerm);
|
||||
$this->fields = $fields;
|
||||
}
|
||||
|
||||
public function hasSearchField(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->fields);
|
||||
}
|
||||
|
||||
public function getSearchField(string $name): ?string
|
||||
{
|
||||
if (!$this->hasSearchField($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
public function getSearchFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
public function getSearchTerm(): ?string
|
||||
{
|
||||
return $this->term;
|
||||
}
|
||||
|
||||
public function hasSearchTerm(): bool
|
||||
{
|
||||
return !empty($this->term);
|
||||
}
|
||||
|
||||
public function getOriginalSearch(): string
|
||||
{
|
||||
return $this->originalTerm;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->originalTerm;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user