chg: [behavior:tags] Custom finder and small improvements
parent
61255e2837
commit
eed5b9226a
|
@ -745,7 +745,7 @@ class CRUDComponent extends Component
|
||||||
'label' => $tags,
|
'label' => $tags,
|
||||||
'forceAnd' => true
|
'forceAnd' => true
|
||||||
])->select($modelAlias . '.id');
|
])->select($modelAlias . '.id');
|
||||||
return $query = $query->where([$modelAlias . '.id IN' => $subQuery]);
|
return $query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
||||||
|
|
|
@ -11,6 +11,7 @@ class TagBehavior extends Behavior
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $_defaultConfig = [
|
protected $_defaultConfig = [
|
||||||
|
'finderField' => 'label',
|
||||||
'tagsAssoc' => [
|
'tagsAssoc' => [
|
||||||
'className' => 'Tags',
|
'className' => 'Tags',
|
||||||
// 'joinTable' => 'tagged', // uncomment me!
|
// 'joinTable' => 'tagged', // uncomment me!
|
||||||
|
@ -148,9 +149,7 @@ class TagBehavior extends Behavior
|
||||||
public function normalizeTags($tags) {
|
public function normalizeTags($tags) {
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
$modelAlias = $this->_table->getAlias();
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
|
||||||
$common = [
|
$common = [
|
||||||
'_joinData' => [
|
'_joinData' => [
|
||||||
'fk_model' => $modelAlias
|
'fk_model' => $modelAlias
|
||||||
|
@ -209,4 +208,150 @@ class TagBehavior extends Behavior
|
||||||
->select('Tags.id');
|
->select('Tags.id');
|
||||||
return $query->first();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -87,7 +87,7 @@ echo $this->Bootstrap->modal([
|
||||||
$filteringTable.find('tbody').empty()
|
$filteringTable.find('tbody').empty()
|
||||||
addControlRow($filteringTable)
|
addControlRow($filteringTable)
|
||||||
const randomValue = getRandomValue()
|
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'] : []
|
const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : []
|
||||||
delete activeFilters['filteringTags']
|
delete activeFilters['filteringTags']
|
||||||
for (let [field, value] of Object.entries(activeFilters)) {
|
for (let [field, value] of Object.entries(activeFilters)) {
|
||||||
|
|
|
@ -1125,7 +1125,7 @@ class HtmlHelper {
|
||||||
options.colour = '#924da6'
|
options.colour = '#924da6'
|
||||||
}
|
}
|
||||||
const $tag = $('<span/>')
|
const $tag = $('<span/>')
|
||||||
.addClass(['tag', 'badge', 'border'])
|
.addClass(['tag', 'badge', 'align-text-top'])
|
||||||
.css({color: getTextColour(options.colour), 'background-color': options.colour})
|
.css({color: getTextColour(options.colour), 'background-color': options.colour})
|
||||||
.text(options.text)
|
.text(options.text)
|
||||||
|
|
||||||
|
|
|
@ -162,14 +162,14 @@ function deleteTag(url, tags, clicked) {
|
||||||
statusNode: $statusNode,
|
statusNode: $statusNode,
|
||||||
skipFeedback: true,
|
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')
|
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
|
$container = $tagContainer // old container might not exist anymore since it was replaced after the refresh
|
||||||
})
|
})
|
||||||
const theToast = UI.toast({
|
const theToast = UI.toast({
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
title: result.message,
|
title: apiResult.message,
|
||||||
bodyHtml: $('<div/>').append(
|
bodyHtml: $('<div/>').append(
|
||||||
$('<span/>').text('Cancel untag operation.'),
|
$('<span/>').text('Cancel untag operation.'),
|
||||||
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ml-3']).text('Restore tag').click(function() {
|
$('<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 = {
|
const APIOptions = {
|
||||||
statusNode: $statusNode
|
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')
|
const $container = $statusNode.closest('.tag-container-wrapper')
|
||||||
refreshTagList(result, $container)
|
refreshTagList(apiResult, $container)
|
||||||
}).catch((e) => {})
|
}).catch((e) => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshTagList(result, $container) {
|
function refreshTagList(apiResult, $container) {
|
||||||
const controllerName = result.url.split('/')[1]
|
const controllerName = apiResult.url.split('/')[1]
|
||||||
const entityId = result.data.id
|
const entityId = apiResult.data.id
|
||||||
const url = `/${controllerName}/viewTags/${entityId}`
|
const url = `/${controllerName}/viewTags/${entityId}`
|
||||||
return UI.reload(url, $container)
|
return UI.reload(url, $container)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue