new: [CRUD] Advanced filtering capabilities for index
parent
97c5f7b197
commit
004bca47e6
|
@ -55,6 +55,14 @@ class CRUDComponent extends Component
|
||||||
$this->Controller->set('data', $data);
|
$this->Controller->set('data', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function filtering(): void
|
||||||
|
{
|
||||||
|
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
|
||||||
|
$this->Controller->set('filters', $filters);
|
||||||
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
|
$this->Controller->render('/genericTemplates/filters');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getResponsePayload Returns the adaquate response payload based on the request context
|
* getResponsePayload Returns the adaquate response payload based on the request context
|
||||||
|
|
|
@ -12,10 +12,13 @@ use Cake\Http\Exception\ForbiddenException;
|
||||||
|
|
||||||
class OrganisationsController extends AppController
|
class OrganisationsController extends AppController
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public $filters = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'filters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'],
|
'filters' => $this->filters,
|
||||||
'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'],
|
'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'],
|
||||||
'contextFilters' => [
|
'contextFilters' => [
|
||||||
'custom' => [
|
'custom' => [
|
||||||
|
@ -64,6 +67,11 @@ class OrganisationsController extends AppController
|
||||||
$this->set('metaGroup', 'ContactDB');
|
$this->set('metaGroup', 'ContactDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function filtering()
|
||||||
|
{
|
||||||
|
$this->CRUD->filtering();
|
||||||
|
}
|
||||||
|
|
||||||
public function add()
|
public function add()
|
||||||
{
|
{
|
||||||
$this->CRUD->add();
|
$this->CRUD->add();
|
||||||
|
|
|
@ -21,10 +21,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'type' => 'search',
|
'type' => 'search',
|
||||||
'button' => __('Filter'),
|
'button' => __('Search'),
|
||||||
'placeholder' => __('Enter value to search'),
|
'placeholder' => __('Enter value to search'),
|
||||||
'data' => '',
|
'data' => '',
|
||||||
'searchKey' => 'value'
|
'searchKey' => 'value',
|
||||||
|
'allowFilering' => true
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
if (!empty($filteringContext['filterCondition'])) { // PHP replaces `.` by `_` when fetching the request parameter
|
if (!empty($filteringContext['filterCondition'])) { // PHP replaces `.` by `_` when fetching the request parameter
|
||||||
$currentFilteringContext = [];
|
$currentFilteringContext = [];
|
||||||
foreach ($filteringContext['filterCondition'] as $currentFilteringContextKey => $value) {
|
foreach ($filteringContext['filterCondition'] as $currentFilteringContextKey => $value) {
|
||||||
$currentFilteringContext[str_replace('.', '_', $currentFilteringContextKey)] = $value;
|
$currentFilteringContextKey = str_replace('.', '_', $currentFilteringContextKey);
|
||||||
|
$currentFilteringContextKey = str_replace(' ', '_', $currentFilteringContextKey);
|
||||||
|
$currentFilteringContext[$currentFilteringContextKey] = $value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$currentFilteringContext = $filteringContext['filterCondition'];
|
$currentFilteringContext = $filteringContext['filterCondition'];
|
||||||
|
|
|
@ -12,12 +12,32 @@
|
||||||
* - id: element ID for the input field - defaults to quickFilterField
|
* - id: element ID for the input field - defaults to quickFilterField
|
||||||
*/
|
*/
|
||||||
if (!isset($data['requirement']) || $data['requirement']) {
|
if (!isset($data['requirement']) || $data['requirement']) {
|
||||||
|
$filteringButton = '';
|
||||||
|
if (!empty($data['allowFilering'])) {
|
||||||
|
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
|
||||||
|
$buttonConfig = [
|
||||||
|
'icon' => 'filter',
|
||||||
|
'params' => [
|
||||||
|
'title' => __('Filter index'),
|
||||||
|
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||||
|
]
|
||||||
|
];
|
||||||
|
if (count($activeFilters) > 0) {
|
||||||
|
$buttonConfig['badge'] = [
|
||||||
|
'variant' => 'light',
|
||||||
|
'text' => count($activeFilters),
|
||||||
|
'title' => __n('There is {0} active filter', 'There are {0} active filters', count($activeFilters), count($activeFilters))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$filteringButton = $this->Bootstrap->button($buttonConfig);
|
||||||
|
}
|
||||||
$button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf(
|
$button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf(
|
||||||
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button></div>',
|
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button>%s</div>',
|
||||||
empty($data['data']) ? '' : h($data['data']),
|
empty($data['data']) ? '' : h($data['data']),
|
||||||
h($tableRandomValue),
|
h($tableRandomValue),
|
||||||
empty($data['fa-icon']) ? '' : sprintf('<i class="fa fa-%s"></i>', h($data['fa-icon'])),
|
empty($data['fa-icon']) ? '' : sprintf('<i class="fa fa-%s"></i>', h($data['fa-icon'])),
|
||||||
empty($data['button']) ? '' : h($data['button'])
|
empty($data['button']) ? '' : h($data['button']),
|
||||||
|
$filteringButton
|
||||||
);
|
);
|
||||||
if (!empty($data['cancel'])) {
|
if (!empty($data['cancel'])) {
|
||||||
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
|
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
|
||||||
|
@ -45,6 +65,7 @@
|
||||||
var action = '<?= $this->request->getParam('action') ?>';
|
var action = '<?= $this->request->getParam('action') ?>';
|
||||||
var additionalUrlParams = '';
|
var additionalUrlParams = '';
|
||||||
var quickFilter = <?= json_encode(!empty($quickFilter) ? $quickFilter : []) ?>;
|
var quickFilter = <?= json_encode(!empty($quickFilter) ? $quickFilter : []) ?>;
|
||||||
|
var activeFilters = <?= json_encode(!empty($activeFilters) ? $activeFilters : []) ?>;
|
||||||
<?php
|
<?php
|
||||||
if (!empty($data['additionalUrlParams'])) {
|
if (!empty($data['additionalUrlParams'])) {
|
||||||
echo sprintf(
|
echo sprintf(
|
||||||
|
@ -75,6 +96,14 @@
|
||||||
$(`#quickFilterField-${randomValue}`).popover('hide')
|
$(`#quickFilterField-${randomValue}`).popover('hide')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(`#toggleFilterButton-${randomValue}`)
|
||||||
|
.data('activeFilters', activeFilters)
|
||||||
|
.click(function() {
|
||||||
|
const url = `/${controller}/filtering`
|
||||||
|
const reloadUrl = `/${controller}/index${additionalUrlParams}`
|
||||||
|
openFilteringModal(this, url, reloadUrl, $(`#table-container-${randomValue}`));
|
||||||
|
})
|
||||||
|
|
||||||
function doFilter($button) {
|
function doFilter($button) {
|
||||||
$(`#quickFilterField-${randomValue}`).popover('hide')
|
$(`#quickFilterField-${randomValue}`).popover('hide')
|
||||||
const encodedFilters = encodeURIComponent($(`#quickFilterField-${randomValue}`).val())
|
const encodedFilters = encodeURIComponent($(`#quickFilterField-${randomValue}`).val())
|
||||||
|
@ -114,5 +143,12 @@
|
||||||
return $table[0].outerHTML
|
return $table[0].outerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFilteringModal(clicked, url, reloadUrl, tableId) {
|
||||||
|
const loadingOverlay = new OverlayFactory(clicked);
|
||||||
|
loadingOverlay.show()
|
||||||
|
UI.openModalFromURL(url, reloadUrl, tableId).finally(() => {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
<?php
|
||||||
|
use Cake\Utility\Inflector;
|
||||||
|
|
||||||
|
$filteringForm = $this->Bootstrap->table(
|
||||||
|
[
|
||||||
|
'small' => true,
|
||||||
|
'striped' => false,
|
||||||
|
'hover' => false,
|
||||||
|
'tableClass' => ['indexFilteringTable'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'fields' => [
|
||||||
|
__('Field'),
|
||||||
|
__('Operator'),
|
||||||
|
[
|
||||||
|
'labelHtml' => sprintf('%s %s',
|
||||||
|
__('Value'),
|
||||||
|
sprintf('<span class="fa fa-info ml-1" title="%s"><span>', __('Supports strict match and LIKE match with the `%` character. Example: `%.com`'))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
__('Action')
|
||||||
|
],
|
||||||
|
'items' => []
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
echo $this->Bootstrap->modal([
|
||||||
|
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
||||||
|
'size' => 'lg',
|
||||||
|
'type' => 'confirm',
|
||||||
|
'bodyHtml' => $filteringForm,
|
||||||
|
'confirmText' => __('Filter'),
|
||||||
|
'confirmFunction' => 'filterIndex(this)'
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(() => {
|
||||||
|
const $filteringTable = $('table.indexFilteringTable')
|
||||||
|
initFilteringTable($filteringTable)
|
||||||
|
})
|
||||||
|
|
||||||
|
function filterIndex(clicked) {
|
||||||
|
const controller = '<?= $this->request->getParam('controller') ?>';
|
||||||
|
const action = 'index';
|
||||||
|
const $clicked = $(clicked)
|
||||||
|
const $tbody = $clicked.closest('div.modal-content').find('table.indexFilteringTable tbody')
|
||||||
|
const $rows = $tbody.find('tr:not(#controlRow)')
|
||||||
|
const activeFilters = {}
|
||||||
|
$rows.each(function() {
|
||||||
|
const rowData = getDataFromRow($(this))
|
||||||
|
let fullFilter = rowData['name']
|
||||||
|
if (rowData['operator'] == '!=') {
|
||||||
|
fullFilter += ' !='
|
||||||
|
}
|
||||||
|
activeFilters[fullFilter] = rowData['value']
|
||||||
|
})
|
||||||
|
const searchParam = (new URLSearchParams(activeFilters)).toString();
|
||||||
|
const url = `/${controller}/${action}?${searchParam}`
|
||||||
|
|
||||||
|
const randomValue = getRandomValue()
|
||||||
|
UI.reload(url, $(`#table-container-${randomValue}`), $(`#table-container-${randomValue} table.table`), [{
|
||||||
|
node: $(`#toggleFilterButton-${randomValue}`),
|
||||||
|
config: {}
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFilteringTable($filteringTable) {
|
||||||
|
const $controlRow = $filteringTable.find('#controlRow')
|
||||||
|
$filteringTable.find('tbody').empty()
|
||||||
|
addControlRow($filteringTable)
|
||||||
|
const randomValue = getRandomValue()
|
||||||
|
const activeFilters = $(`#toggleFilterButton-${randomValue}`).data('activeFilters')
|
||||||
|
for (let [field, value] of Object.entries(activeFilters)) {
|
||||||
|
const fieldParts = field.split(' ')
|
||||||
|
let operator = '='
|
||||||
|
if (fieldParts.length == 2 && fieldParts[1] == '!=') {
|
||||||
|
operator = '!='
|
||||||
|
field = fieldParts[0]
|
||||||
|
} else if (fieldParts.length > 2) {
|
||||||
|
console.error('Field contains multiple spaces. ' + field)
|
||||||
|
}
|
||||||
|
addFilteringRow($filteringTable, field, value, operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addControlRow($filteringTable) {
|
||||||
|
const availableFilters = <?= json_encode($filters) ?>;
|
||||||
|
const $selectField = $('<select/>').addClass('fieldSelect custom-select custom-select-sm')
|
||||||
|
availableFilters.forEach(filter => {
|
||||||
|
$selectField.append($('<option/>').text(filter))
|
||||||
|
});
|
||||||
|
const $selectOperator = $('<select/>').addClass('fieldOperator custom-select custom-select-sm')
|
||||||
|
.append([
|
||||||
|
$('<option/>').text('=').val('='),
|
||||||
|
$('<option/>').text('!=').val('!='),
|
||||||
|
])
|
||||||
|
const $row = $('<tr/>').attr('id', 'controlRow')
|
||||||
|
.append(
|
||||||
|
$('<td/>').append($selectField),
|
||||||
|
$('<td/>').append($selectOperator),
|
||||||
|
$('<td/>').append(
|
||||||
|
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm')
|
||||||
|
),
|
||||||
|
$('<td/>').append(
|
||||||
|
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-primary')
|
||||||
|
.append($('<span/>').addClass('fa fa-plus'))
|
||||||
|
.click(addFiltering)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
$filteringTable.append($row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFilteringRow($filteringTable, field, value, operator) {
|
||||||
|
const $selectOperator = $('<select/>').addClass('fieldOperator custom-select custom-select-sm')
|
||||||
|
.append([
|
||||||
|
$('<option/>').text('=').val('='),
|
||||||
|
$('<option/>').text('!=').val('!='),
|
||||||
|
]).val(operator)
|
||||||
|
const $row = $('<tr/>')
|
||||||
|
.append(
|
||||||
|
$('<td/>').text(field).addClass('fieldName').data('fieldName', field),
|
||||||
|
$('<td/>').append($selectOperator),
|
||||||
|
$('<td/>').append(
|
||||||
|
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm').val(value)
|
||||||
|
),
|
||||||
|
$('<td/>').append(
|
||||||
|
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-danger')
|
||||||
|
.append($('<span/>').addClass('fa fa-trash'))
|
||||||
|
.click(removeSelf)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
$filteringTable.append($row)
|
||||||
|
const $controlRow = $filteringTable.find('#controlRow')
|
||||||
|
disableOptionFromSelect($controlRow, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFiltering() {
|
||||||
|
const $table = $(this).closest('table.indexFilteringTable')
|
||||||
|
const $controlRow = $table.find('#controlRow')
|
||||||
|
const field = $controlRow.find('select.fieldSelect').val()
|
||||||
|
const value = $controlRow.find('input.fieldValue').val()
|
||||||
|
const operator = $controlRow.find('input.fieldOperator').val()
|
||||||
|
addFilteringRow($table, field, value, operator)
|
||||||
|
$controlRow.find('input.fieldValue').val('')
|
||||||
|
$controlRow.find('select.fieldSelect').val('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelf() {
|
||||||
|
const $row = $(this).closest('tr')
|
||||||
|
const $controlRow = $row.closest('table.indexFilteringTable').find('#controlRow')
|
||||||
|
const field = $row.data('fieldName')
|
||||||
|
$row.remove()
|
||||||
|
enableOptionFromSelect($controlRow, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableOptionFromSelect($controlRow, optionName) {
|
||||||
|
$controlRow.find('select.fieldSelect option').each(function() {
|
||||||
|
const $option = $(this)
|
||||||
|
if ($option.text() == optionName) {
|
||||||
|
$option.prop('disabled', true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableOptionFromSelect($controlRow, optionName) {
|
||||||
|
$controlRow.find('select.fieldSelect option').each(function() {
|
||||||
|
const $option = $(this)
|
||||||
|
if ($option.text() == optionName) {
|
||||||
|
$option.prop('disabled', false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataFromRow($row) {
|
||||||
|
const rowData = {};
|
||||||
|
rowData['name'] = $row.find('td.fieldName').data('fieldName')
|
||||||
|
rowData['operator'] = $row.find('select.fieldOperator').val()
|
||||||
|
rowData['value'] = $row.find('input.fieldValue').val()
|
||||||
|
return rowData
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomValue() {
|
||||||
|
const $container = $('div[id^="table-container-"]')
|
||||||
|
const randomValue = $container.attr('id').split('-')[2]
|
||||||
|
return randomValue
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in New Issue