new: [CRUD] Advanced filtering capabilities for index

pull/40/head
mokaddem 2021-03-10 09:43:36 +01:00
parent 97c5f7b197
commit 004bca47e6
6 changed files with 249 additions and 6 deletions

View File

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

View File

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

View File

@ -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
]
]
],

View File

@ -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'];

View File

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

View File

@ -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.&#10;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>