chg: [behavior:tags] Custom finder and small improvements

pull/72/head
mokaddem 2021-09-01 16:12:56 +02:00
parent 61255e2837
commit eed5b9226a
5 changed files with 158 additions and 13 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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)) {

View File

@ -1125,7 +1125,7 @@ class HtmlHelper {
options.colour = '#924da6'
}
const $tag = $('<span/>')
.addClass(['tag', 'badge', 'border'])
.addClass(['tag', 'badge', 'align-text-top'])
.css({color: getTextColour(options.colour), 'background-color': options.colour})
.text(options.text)

View File

@ -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: $('<div/>').append(
$('<span/>').text('Cancel untag operation.'),
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ml-3']).text('Restore tag').click(function() {
@ -193,15 +193,15 @@ function addTags(url, tags, $statusNode) {
const APIOptions = {
statusNode: $statusNode
}
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => {
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => {
const $container = $statusNode.closest('.tag-container-wrapper')
refreshTagList(result, $container)
refreshTagList(apiResult, $container)
}).catch((e) => {})
}
function refreshTagList(result, $container) {
const controllerName = result.url.split('/')[1]
const entityId = result.data.id
function refreshTagList(apiResult, $container) {
const controllerName = apiResult.url.split('/')[1]
const entityId = apiResult.data.id
const url = `/${controllerName}/viewTags/${entityId}`
return UI.reload(url, $container)
}