diff --git a/asset/js/widget/FilterInput.js b/asset/js/widget/FilterInput.js index 97a35778..1f8df7ab 100644 --- a/asset/js/widget/FilterInput.js +++ b/asset/js/widget/FilterInput.js @@ -998,6 +998,10 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { let label = super.renderTerm(termData, termIndex); label.dataset.type = termData.type; + if (!! termData.dataType) { + label.firstChild.type = termData.dataType; + } + if (! termData.class) { label.classList.add(termData.type); } diff --git a/src/Compat/SearchControls.php b/src/Compat/SearchControls.php index c40204d7..c48ec5a7 100644 --- a/src/Compat/SearchControls.php +++ b/src/Compat/SearchControls.php @@ -84,7 +84,7 @@ public function createSearchBar(Query $query, ...$params): SearchBar } $filterColumns = $this->fetchFilterColumns($query); - $columnValidator = function (SearchBar\ValidatedColumn $column) use ($query, $filterColumns) { + $columnValidator = function ($column, $operator, $value, $condition) use ($query, $filterColumns) { $searchPath = $column->getSearchValue(); if (strpos($searchPath, '.') === false) { $column->setSearchValue($query->getResolver()->qualifyPath( @@ -108,6 +108,12 @@ public function createSearchBar(Query $query, ...$params): SearchBar if (isset($definition)) { $column->setLabel($definition->getLabel()); + + if (! $definition->isValidValue($condition)) { + $value->setMessage(t('Is not a valid value')); + } else { + $value->setLabel($definition->getValueLabel($value->getSearchValue())); + } } }; @@ -257,13 +263,15 @@ protected function enrichFilterCondition(Filter\Condition $condition, Query $que } try { - $label = $query->getResolver()->getColumnDefinition($path)->getLabel(); + $definition = $query->getResolver()->getColumnDefinition($path); } catch (InvalidRelationException $_) { - $label = null; + // pass } - if (isset($label)) { - $condition->metaData()->set('columnLabel', $label); + if (isset($definition)) { + $condition->metaData()->set('columnLabel', $definition->getLabel()); + $condition->metaData()->set('valueLabel', $definition->getValueLabel($condition->getValue())); + //$condition->metaData()->set('valueType', $definition->getType()); } } } diff --git a/src/Control/SearchBar.php b/src/Control/SearchBar.php index 2b18a834..99914e78 100644 --- a/src/Control/SearchBar.php +++ b/src/Control/SearchBar.php @@ -229,24 +229,27 @@ public function isValidEvent($event) private function validateCondition($eventType, $indices, $termsData, &$changes) { - // TODO: In case of the query string validation, all three are guaranteed to be set. - // The Parser also provides defaults, why shouldn't we here? $column = ValidatedColumn::fromTermData($termsData[0]); $operator = isset($termsData[1]) ? ValidatedOperator::fromTermData($termsData[1]) - : null; + : new ValidatedOperator('='); $value = isset($termsData[2]) ? ValidatedValue::fromTermData($termsData[2]) - : null; + : new ValidatedValue(true); + $condition = QueryString::createCondition( + $column->getSearchValue(), + $operator->getSearchValue(), + $value->getSearchValue() + ); - $this->emit($eventType, [$column, $operator, $value]); + $this->emit($eventType, [$column, $operator, $value, $condition]); if ($eventType !== self::ON_REMOVE) { if (! $column->isValid() || $column->hasBeenChanged()) { $changes[$indices[0]] = array_merge($termsData[0], $column->toTermData()); } - if ($operator && ! $operator->isValid()) { + if ($operator && (! $operator->isValid() || $operator->hasBeenChanged())) { $changes[$indices[1]] = array_merge($termsData[1], $operator->toTermData()); } @@ -397,14 +400,18 @@ protected function assemble() $column = ValidatedColumn::fromFilterCondition($condition); $operator = ValidatedOperator::fromFilterCondition($condition); $value = ValidatedValue::fromFilterCondition($condition); - $this->emit(self::ON_ADD, [$column, $operator, $value]); + + // $condition is cloned as validators shouldn't be able to change it directly + $this->emit(self::ON_ADD, [$column, $operator, $value, clone $condition]); $condition->setColumn($column->getSearchValue()); $condition->setValue($value->getSearchValue()); - if (! $column->isValid()) { + if (! $column->isValid() || ! $operator->isValid() || ! $value->isValid()) { $invalid = true; + } + if (! $column->isValid() || $column->hasBeenChanged()) { if ($submitted) { $condition->metaData()->merge($column->toMetaData()); } else { @@ -412,9 +419,7 @@ protected function assemble() } } - if (! $operator->isValid()) { - $invalid = true; - + if (! $operator->isValid() || $operator->hasBeenChanged()) { if ($submitted) { $condition->metaData()->merge($operator->toMetaData()); } else { @@ -422,9 +427,7 @@ protected function assemble() } } - if (! $value->isValid()) { - $invalid = true; - + if (! $value->isValid() || $value->hasBeenChanged()) { if ($submitted) { $condition->metaData()->merge($value->toMetaData()); } else { diff --git a/src/Control/SearchBar/Suggestions.php b/src/Control/SearchBar/Suggestions.php index 708d846d..4081558c 100644 --- a/src/Control/SearchBar/Suggestions.php +++ b/src/Control/SearchBar/Suggestions.php @@ -8,9 +8,10 @@ use ipl\Html\BaseHtmlElement; use ipl\Html\FormattedString; use ipl\Html\FormElement\ButtonElement; -use ipl\Html\FormElement\InputElement; use ipl\Html\HtmlElement; use ipl\Html\Text; +use ipl\Orm\ColumnDefinition; +use ipl\Orm\Query; use ipl\Stdlib\Contract\Paginatable; use ipl\Stdlib\Filter; use ipl\Web\Control\SearchEditor; @@ -43,6 +44,9 @@ abstract class Suggestions extends BaseHtmlElement /** @var string */ protected $failureMessage; + /** @var ColumnDefinition */ + protected $columnDefinition; + public function setSearchTerm($term) { $this->searchTerm = $term; @@ -78,6 +82,20 @@ public function setFailureMessage($message) return $this; } + public function setColumnDefinition(ColumnDefinition $definition): self + { + $this->columnDefinition = $definition; + + return $this; + } + + /** + * Get the query that's used to fetch suggestions + * + * @return Query + */ + abstract public function getQuery(): Query; + /** * Return whether the relation should be shown for the given column * @@ -240,9 +258,9 @@ protected function assemble() $data = new LimitIterator(new IteratorIterator($this->data), 0, self::DEFAULT_LIMIT); } - foreach ($data as $term => $meta) { + foreach ($data as $term => $label) { if (is_int($term)) { - $term = $meta; + $term = $label; } $attributes = [ @@ -255,19 +273,15 @@ protected function assemble() $attributes['data-type'] = $this->type; } - if (is_array($meta)) { - foreach ($meta as $key => $value) { - if ($key === 'label') { - $label = $value; - } - - $attributes['data-' . $key] = $value; - } - } else { - $label = $meta; - $attributes['data-label'] = $meta; + if ($this->type === 'value') { + $label = $this->columnDefinition->getValueLabel($term); + //$attributes['data-data-type'] = $this->columnDefinition->getType(); + //$attributes['data-min-size'] = $this->columnDefinition->getMin(); + //$attributes['data-max-size'] = $this->columnDefinition->getMax(); } + $attributes['data-label'] = $label; + $button = (new ButtonElement(null, $attributes)) ->setAttribute('value', $label) ->addHtml(Text::create($label)); @@ -342,11 +356,13 @@ public function forRequest(ServerRequestInterface $request) break; } - $searchFilter = QueryString::parse( - isset($requestData['searchFilter']) - ? $requestData['searchFilter'] - : '' + $this->setColumnDefinition( + $this->getQuery() + ->getResolver() + ->getColumnDefinition($requestData['column']) ); + + $searchFilter = QueryString::parse($requestData['searchFilter'] ?? ''); if ($searchFilter instanceof Filter\Condition) { $searchFilter = Filter::all($searchFilter); } @@ -358,7 +374,15 @@ public function forRequest(ServerRequestInterface $request) } if ($search) { - $this->setDefault(['search' => $label]); + if ($requestData['operator'] !== '=' && $requestData['operator'] !== '!=') { + // The transferred label usually contains automatically added wildcards. + // The search term on the other hand may also have some, but then they + // were explicitly added by the user. This ensures only the latter is + // suggested as default. + $this->setDefault(['search' => $search]); + } else { + $this->setDefault(['search' => $label]); + } } break; diff --git a/src/Control/SearchBar/Terms.php b/src/Control/SearchBar/Terms.php index c81e3360..1082d139 100644 --- a/src/Control/SearchBar/Terms.php +++ b/src/Control/SearchBar/Terms.php @@ -153,6 +153,7 @@ protected function assembleCondition(Filter\Condition $filter, BaseHtmlElement $ $operator = QueryString::getRuleSymbol($filter); $value = $filter->getValue(); $columnLabel = $filter->metaData()->get('columnLabel', $column); + $valueLabel = $filter->metaData()->get('valueLabel', $value); $group = new HtmlElement( 'div', @@ -197,7 +198,7 @@ protected function assembleCondition(Filter\Condition $filter, BaseHtmlElement $ 'class' => 'value', 'type' => 'value', 'search' => rawurlencode($value), - 'label' => $value + 'label' => $valueLabel ]; if ($filter->metaData()->has('invalidValuePattern')) { $valueData['pattern'] = $filter->metaData()->get('invalidValuePattern'); @@ -206,6 +207,10 @@ protected function assembleCondition(Filter\Condition $filter, BaseHtmlElement $ } } + if ($filter->metaData()->has('valueType')) { + $valueData['data-type'] = $filter->metaData()->get('valueType'); + } + $this->assembleTerm($valueData, $group); } } @@ -248,6 +253,11 @@ protected function assembleTerm(array $data, BaseHtmlElement $where) } } + if (isset($data['data-type'])) { + $term->setAttribute('data-data-type', $data['data-type']); + $term->getFirst('input')->setAttribute('type', $data['data-type']); + } + $where->addHtml($term); return $term; diff --git a/src/Control/SearchBar/ValidatedTerm.php b/src/Control/SearchBar/ValidatedTerm.php index e5525523..f9bf545e 100644 --- a/src/Control/SearchBar/ValidatedTerm.php +++ b/src/Control/SearchBar/ValidatedTerm.php @@ -7,7 +7,7 @@ abstract class ValidatedTerm { /** @var string The default validation constraint */ - const DEFAULT_PATTERN = '^\s*(?!%s\b).*\s*$'; + const DEFAULT_PATTERN = '^\s*(?!%s).*\s*$'; /** @var mixed The search value */ protected $searchValue; @@ -152,7 +152,17 @@ public function setMessage($message) public function getPattern() { if ($this->pattern === null) { - return sprintf(self::DEFAULT_PATTERN, $this->getSearchValue()); + // The search value might contain special characters. preg_quote can't be used here, + // since this is not a PCRE pattern and is evaluated by browsers. The pattern used + // here is from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + $searchValue = preg_replace('/[.*+?^${}()|[\\]\\\\]/', '\\\\$0', $this->getSearchValue()); + + if (ctype_alnum(substr($searchValue, -1))) { + // The word boundary check is only necessary when dealing with ... words + $searchValue .= '\\b'; + } + + return sprintf(self::DEFAULT_PATTERN, $searchValue); } return $this->pattern; @@ -183,7 +193,7 @@ public function toTermData() 'search' => $this->getSearchValue(), 'label' => $this->getLabel() ?: $this->getSearchValue(), 'invalidMsg' => $this->getMessage(), - 'pattern' => $this->getPattern() + 'pattern' => $this->isValid() ? null : $this->getPattern() ]; } diff --git a/src/Filter/Parser.php b/src/Filter/Parser.php index 248b41c8..09ba4f2b 100644 --- a/src/Filter/Parser.php +++ b/src/Filter/Parser.php @@ -512,30 +512,7 @@ protected function nextChar() */ protected function createCondition($column, $operator, $value) { - $column = trim($column); - - switch ($operator) { - case '=': - if (is_string($value) && strpos($value, "*") !== false) { - return Filter::like($column, $value); - } - - return Filter::equal($column, $value); - case '!=': - if (is_string($value) && strpos($value, '*') !== false) { - return Filter::unlike($column, $value); - } - - return Filter::unequal($column, $value); - case '>': - return Filter::greaterThan($column, $value); - case '>=': - return Filter::greaterThanOrEqual($column, $value); - case '<': - return Filter::lessThan($column, $value); - case '<=': - return Filter::lessThanOrEqual($column, $value); - } + return QueryString::createCondition($column, $operator, $value); } /** diff --git a/src/Filter/QueryString.php b/src/Filter/QueryString.php index 235bf38d..f52500fb 100644 --- a/src/Filter/QueryString.php +++ b/src/Filter/QueryString.php @@ -89,4 +89,44 @@ public static function getRuleSymbol(Filter\Rule $rule) throw new InvalidArgumentException('Unknown rule type provided'); } } + + /** + * Create and return a condition + * + * @param string $column + * @param string $operator + * @param mixed $value + * + * @return Filter\Condition + * @throws InvalidArgumentException In case the operator is invalid + */ + public static function createCondition(string $column, string $operator, $value): Filter\Condition + { + $column = trim($column); + + switch ($operator) { + case '=': + if (is_string($value) && strpos($value, "*") !== false) { + return Filter::like($column, $value); + } + + return Filter::equal($column, $value); + case '!=': + if (is_string($value) && strpos($value, '*') !== false) { + return Filter::unlike($column, $value); + } + + return Filter::unequal($column, $value); + case '>': + return Filter::greaterThan($column, $value); + case '>=': + return Filter::greaterThanOrEqual($column, $value); + case '<': + return Filter::lessThan($column, $value); + case '<=': + return Filter::lessThanOrEqual($column, $value); + default: + throw new InvalidArgumentException(sprintf("Invalid operator '%s'", $operator)); + } + } }