From eed5b9226a6ae88788aeb941376ce0320dc4990b Mon Sep 17 00:00:00 2001 From: mokaddem Date: Wed, 1 Sep 2021 16:12:56 +0200 Subject: [PATCH] chg: [behavior:tags] Custom finder and small improvements --- src/Controller/Component/CRUDComponent.php | 2 +- src/Model/Behavior/TagBehavior.php | 149 ++++++++++++++++++++- templates/genericTemplates/filters.php | 2 +- webroot/js/bootstrap-helper.js | 2 +- webroot/js/main.js | 16 +-- 5 files changed, 158 insertions(+), 13 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 51cdbfc..75f02ba 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -745,7 +745,7 @@ class CRUDComponent extends Component 'label' => $tags, 'forceAnd' => true ])->select($modelAlias . '.id'); - return $query = $query->where([$modelAlias . '.id IN' => $subQuery]); + return $query->where([$modelAlias . '.id IN' => $subQuery]); } protected function setNestedRelatedCondition($query, $filterParts, $filterValue) diff --git a/src/Model/Behavior/TagBehavior.php b/src/Model/Behavior/TagBehavior.php index 63dd57d..437e74c 100644 --- a/src/Model/Behavior/TagBehavior.php +++ b/src/Model/Behavior/TagBehavior.php @@ -11,6 +11,7 @@ class TagBehavior extends Behavior { protected $_defaultConfig = [ + 'finderField' => 'label', 'tagsAssoc' => [ 'className' => 'Tags', // 'joinTable' => 'tagged', // uncomment me! @@ -148,9 +149,7 @@ class TagBehavior extends Behavior public function normalizeTags($tags) { $result = []; - $modelAlias = $this->_table->getAlias(); - $common = [ '_joinData' => [ 'fk_model' => $modelAlias @@ -209,4 +208,150 @@ class TagBehavior extends Behavior ->select('Tags.id'); return $query->first(); } + + public function findByTag(Query $query, array $options) { + $finderField = $optionsKey = $this->getConfig('finderField'); + if (!$finderField) { + $finderField = $optionsKey = 'label'; + } + + if (!isset($options[$optionsKey])) { + throw new RuntimeException(__('Expected key `{0}` not present in find(\'tagged\') options argument.', $optionsKey)); + } + $isAndOperator = isset($options['OperatorAND']) ? $options['OperatorAND'] : true; + $filterValue = $options[$optionsKey]; + if (!$filterValue) { + return $query; + } + + $filterValue = $this->dissectArgs($filterValue); + if (!empty($filterValue['NOT']) || !empty($filterValue['LIKE'])) { + return $this->findByComplexQueryConditions($query, $filterValue, $finderField, $isAndOperator); + } + + $subQuery = $this->buildQuerySnippet($filterValue, $finderField, $isAndOperator); + if (is_string($subQuery)) { + $query->matching('Tags', function ($q) use ($finderField, $subQuery) { + $key = 'Tags.' . $finderField; + return $q->where([ + $key => $subQuery, + ]); + }); + return $query; + } + + $modelAlias = $this->_table->getAlias(); + return $query->where([$modelAlias . '.id IN' => $subQuery]); + } + + public function findUntagged(Query $query, array $options) { + $modelAlias = $this->_table->getAlias(); + $foreignKey = $this->getConfig('tagsAssoc.foreignKey'); + $conditions = ['fk_model' => $modelAlias]; + $this->_table->hasOne('NoTags', [ + 'className' => $this->getConfig('taggedAssoc.className'), + 'foreignKey' => $foreignKey, + 'conditions' => $conditions + ]); + $query = $query->contain(['NoTags'])->where(['NoTags.id IS' => null]); + return $query; + } + + protected function dissectArgs($filterValue): array + { + if (!is_array($filterValue)) { + return $filterValue; + } + $dissected = [ + 'AND' => [], + 'NOT' => [], + 'LIKE' => [], + ]; + foreach ($filterValue as $value) { + if (substr($value, 0, 1) == '!') { + $dissected['NOT'][] = substr($value, 1); + } + else if (strpos($value, '%') != false) { + $dissected['LIKE'][] = $value; + } else { + $dissected['AND'][] = $value; + } + } + if (empty($dissected['NOT']) && empty($dissected['LIKE'])) { + return $dissected['AND']; + } + return $dissected; + } + + protected function buildQuerySnippet($filterValue, string $finderField, bool $OperatorAND=true) + { + if (!is_array($filterValue)) { + return $filterValue; + } + $key = 'Tags.' . $finderField; + $foreignKey = $this->getConfig('tagsAssoc.foreignKey'); + $conditions = [ + $key . ' IN' => $filterValue, + ]; + + $query = $this->_table->Tagged->find(); + if ($OperatorAND) { + $query->contain(['Tags']) + ->group('Tagged.' . $foreignKey) + ->having('COUNT(*) = ' . count($filterValue)) + ->select('Tagged.' . $foreignKey) + ->where($conditions); + } else { + $query->contain(['Tags']) + ->select('Tagged.' . $foreignKey) + ->where($conditions); + } + return $query; + } + + protected function findByComplexQueryConditions($query, $filterValue, string $finderField, bool $OperatorAND=true) + { + $key = 'Tags.' . $finderField; + $taggedAlias = 'Tagged'; + $foreignKey = $this->getConfig('tagsAssoc.foreignKey'); + + if (!empty($filterValue['AND'])) { + $subQuery = $this->buildQuerySnippet($filterValue['AND'], $finderField, $OperatorAND); + $modelAlias = $this->_table->getAlias(); + $query->where([$modelAlias . '.id IN' => $subQuery]); + } + + if (!empty($filterValue['NOT'])) { + $subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, false); + $modelAlias = $this->_table->getAlias(); + $query->where([$modelAlias . '.id NOT IN' => $subQuery]); + } + + if (!empty($filterValue['LIKE'])) { + $conditions = ['OR' => []]; + foreach($filterValue['LIKE'] as $likeValue) { + $conditions['OR'][] = [ + $key . ' LIKE' => $likeValue, + ]; + } + $subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, $OperatorAND); + if ($OperatorAND) { + $subQuery = $this->_table->Tagged->find() + ->contain(['Tags']) + ->group('Tagged.' . $foreignKey) + ->having('COUNT(*) >= ' . count($filterValue['LIKE'])) + ->select('Tagged.' . $foreignKey) + ->where($conditions); + } else { + $subQuery = $this->_table->Tagged->find() + ->contain(['Tags']) + ->select('Tagged.' . $foreignKey) + ->where($conditions); + } + $modelAlias = $this->_table->getAlias(); + $query->where([$modelAlias . '.id IN' => $subQuery]); + } + + return $query; + } } \ No newline at end of file diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index 3be1b09..9a7c49a 100644 --- a/templates/genericTemplates/filters.php +++ b/templates/genericTemplates/filters.php @@ -87,7 +87,7 @@ echo $this->Bootstrap->modal([ $filteringTable.find('tbody').empty() addControlRow($filteringTable) const randomValue = getRandomValue() - const activeFilters = $(`#toggleFilterButton-${randomValue}`).data('activeFilters') + const activeFilters = Object.assign({}, $(`#toggleFilterButton-${randomValue}`).data('activeFilters')) const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : [] delete activeFilters['filteringTags'] for (let [field, value] of Object.entries(activeFilters)) { diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index cc740d3..39cc6ce 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -1125,7 +1125,7 @@ class HtmlHelper { options.colour = '#924da6' } const $tag = $('') - .addClass(['tag', 'badge', 'border']) + .addClass(['tag', 'badge', 'align-text-top']) .css({color: getTextColour(options.colour), 'background-color': options.colour}) .text(options.text) diff --git a/webroot/js/main.js b/webroot/js/main.js index eca62cc..3acc35f 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -162,14 +162,14 @@ function deleteTag(url, tags, clicked) { statusNode: $statusNode, skipFeedback: true, } - return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => { + return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => { let $container = $statusNode.closest('.tag-container-wrapper') - refreshTagList(result, $container).then(($tagContainer) => { + refreshTagList(apiResult, $container).then(($tagContainer) => { $container = $tagContainer // old container might not exist anymore since it was replaced after the refresh }) const theToast = UI.toast({ variant: 'success', - title: result.message, + title: apiResult.message, bodyHtml: $('
').append( $('').text('Cancel untag operation.'), $('