new: [CRUD] Advanced filtering capabilities for index
parent
97c5f7b197
commit
004bca47e6
|
@ -55,6 +55,14 @@ class CRUDComponent extends Component
|
|||
$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
|
||||
|
|
|
@ -12,10 +12,13 @@ use Cake\Http\Exception\ForbiddenException;
|
|||
|
||||
class OrganisationsController extends AppController
|
||||
{
|
||||
|
||||
public $filters = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
|
||||
|
||||
public function 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'],
|
||||
'contextFilters' => [
|
||||
'custom' => [
|
||||
|
@ -64,6 +67,11 @@ class OrganisationsController extends AppController
|
|||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function filtering()
|
||||
{
|
||||
$this->CRUD->filtering();
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add();
|
||||
|
|
|
@ -21,10 +21,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
if (!empty($filteringContext['filterCondition'])) { // PHP replaces `.` by `_` when fetching the request parameter
|
||||
$currentFilteringContext = [];
|
||||
foreach ($filteringContext['filterCondition'] as $currentFilteringContextKey => $value) {
|
||||
$currentFilteringContext[str_replace('.', '_', $currentFilteringContextKey)] = $value;
|
||||
$currentFilteringContextKey = str_replace('.', '_', $currentFilteringContextKey);
|
||||
$currentFilteringContextKey = str_replace(' ', '_', $currentFilteringContextKey);
|
||||
$currentFilteringContext[$currentFilteringContextKey] = $value;
|
||||
}
|
||||
} else {
|
||||
$currentFilteringContext = $filteringContext['filterCondition'];
|
||||
|
|
|
@ -12,12 +12,32 @@
|
|||
* - id: element ID for the input field - defaults to quickFilterField
|
||||
*/
|
||||
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(
|
||||
'<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']),
|
||||
h($tableRandomValue),
|
||||
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'])) {
|
||||
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
|
||||
|
@ -45,6 +65,7 @@
|
|||
var action = '<?= $this->request->getParam('action') ?>';
|
||||
var additionalUrlParams = '';
|
||||
var quickFilter = <?= json_encode(!empty($quickFilter) ? $quickFilter : []) ?>;
|
||||
var activeFilters = <?= json_encode(!empty($activeFilters) ? $activeFilters : []) ?>;
|
||||
<?php
|
||||
if (!empty($data['additionalUrlParams'])) {
|
||||
echo sprintf(
|
||||
|
@ -75,6 +96,14 @@
|
|||
$(`#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) {
|
||||
$(`#quickFilterField-${randomValue}`).popover('hide')
|
||||
const encodedFilters = encodeURIComponent($(`#quickFilterField-${randomValue}`).val())
|
||||
|
@ -114,5 +143,12 @@
|
|||
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>
|
||||
|
|
|
@ -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