3
0

Add options to extended drilldown filtering

This commit is contained in:
Kelani Tolulope 2024-05-08 12:24:08 +01:00 committed by Jože Fortun
parent 5c1fb4a933
commit 4c2afef058
6 changed files with 327 additions and 69 deletions

View File

@ -48,24 +48,6 @@
:key="`${groupIndex}-${index}`" :key="`${groupIndex}-${index}`"
class="pb-2" class="pb-2"
> >
<b-td
class="align-middle"
style="width: 1%;"
>
<h6
v-if="index === 0"
class="mb-0"
>
{{ $t('recordList.filter.where') }}
</h6>
<b-form-select
v-else
v-model="filter.condition"
:options="conditions"
/>
</b-td>
<b-td <b-td
class="px-2" class="px-2"
> >
@ -156,7 +138,7 @@
<b-tr :key="`addFilter-${groupIndex}`"> <b-tr :key="`addFilter-${groupIndex}`">
<b-td class="pb-0"> <b-td class="pb-0">
<b-button <b-button
variant="link text-decoration-none" variant="link text-decoration-none d-block mr-auto"
style="min-height: 38px; min-width: 84px;" style="min-height: 38px; min-width: 84px;"
@click="addFilter(groupIndex)" @click="addFilter(groupIndex)"
> >
@ -181,15 +163,10 @@
<div <div
class="group-separator" class="group-separator"
> >
<b-form-select <div style="height: 20px; width: 100%;" />
v-if="filterGroup.groupCondition"
v-model="filterGroup.groupCondition"
class="w-auto"
:options="conditions"
/>
<b-button <b-button
v-else v-if="groupIndex === (componentFilter.length - 1)"
variant="outline-primary" variant="outline-primary"
class="btn-add-group bg-white py-2 px-3" class="btn-add-group bg-white py-2 px-3"
@click="addGroup()" @click="addGroup()"

View File

@ -42,6 +42,14 @@ export default {
} }
}, },
computed: {
isDrillDownEnabled () {
if (!this.options) return false
return this.options.drillDown && this.options.drillDown.enabled
},
},
watch: { watch: {
options: { options: {
deep: true, deep: true,
@ -65,6 +73,7 @@ export default {
methods: { methods: {
...mapActions({ ...mapActions({
findChartByID: 'chart/findByID', findChartByID: 'chart/findByID',
findModuleByID: 'module/findByID',
}), }),
createEvents () { createEvents () {
@ -98,6 +107,20 @@ export default {
return this.findChartByID({ chartID, namespaceID, ...params }).then((chart) => { return this.findChartByID({ chartID, namespaceID, ...params }).then((chart) => {
this.chart = chart this.chart = chart
if (this.isDrillDownEnabled) {
const { moduleID, dimensions = [] } = this.chart.config.reports[0] || {}
this.findModuleByID({ namespace: this.namespace, moduleID }).then(chartModule => {
if (!chartModule) {
return
}
const { field } = dimensions[0] || {}
const { name, label } = chartModule.fields.find(({ name }) => name === field) || {}
this.filter.field = { name, label }
})
}
}).catch(this.toastErrorHandler(this.$t('chart.loadFailed'))) }).catch(this.toastErrorHandler(this.$t('chart.loadFailed')))
}, },
@ -158,12 +181,11 @@ export default {
} }
// Get recordListID that is linked // Get recordListID that is linked
let { moduleID, dimensions, filter } = this.filter let { moduleID, dimensions, filter, field } = this.filter
const { name, label } = field || {}
// Construct filter // Construct filter
const dimensionFilter = dimensions ? `(${dimensions} = '${drillDownValue}')` : '' const dimensionFilter = dimensions ? `(${dimensions} = '${drillDownValue}')` : ''
filter = filter ? `(${filter})` : ''
const prefilter = [dimensionFilter, filter].filter(f => f).join(' AND ')
if (drillDown.blockID) { if (drillDown.blockID) {
// Use linked record list to display drill down data // Use linked record list to display drill down data
@ -172,8 +194,15 @@ export default {
// Construct its uniqueID to identify it // Construct its uniqueID to identify it
const recordListUniqueID = [pageID, recordID, drillDown.blockID, false].map(v => v || NoID).join('-') const recordListUniqueID = [pageID, recordID, drillDown.blockID, false].map(v => v || NoID).join('-')
this.$root.$emit(`drill-down-recordList:${recordListUniqueID}`, prefilter) this.$root.$emit(`drill-down-recordList:${recordListUniqueID}`, {
prefilter: dimensionFilter,
name: name || label || dimensions,
value: drillDownValue,
})
} else { } else {
filter = filter ? `(${filter})` : ''
const prefilter = [dimensionFilter, filter].filter(f => f).join(' AND ')
const { title } = this.block const { title } = this.block
const { fields = [] } = this.options.drillDown.recordListOptions || {} const { fields = [] } = this.options.drillDown.recordListOptions || {}

View File

@ -89,7 +89,6 @@
> >
<button <button
class="dropdown-item" class="dropdown-item"
:disabled="activeFilters.includes(f.name)"
@click="updateFilter(f.filter, f.name)" @click="updateFilter(f.filter, f.name)"
> >
{{ f.name }} {{ f.name }}
@ -111,7 +110,6 @@
@updateFields="onUpdateFields" @updateFields="onUpdateFields"
/> />
</div> </div>
<div <div
v-if="!options.hideSearch" v-if="!options.hideSearch"
class="flex-fill" class="flex-fill"
@ -125,29 +123,112 @@
</b-row> </b-row>
<div <div
v-if="activeFilters.length || drillDownFilter || options.showDeletedRecordsOption" v-if="drillDownFilter || options.showDeletedRecordsOption || groupRecordListFilter.length"
class="d-flex" class="d-block gap-1"
> >
<div <div
v-if="activeFilters.length" v-if="groupRecordListFilter.length"
class="d-flex align-items-center flex-wrap" class="d-flex flex-wrap gap-2"
> >
{{ $t('recordList.filter.filters.active') }} <div
<b-form-tags v-for="(filterGroup, groupIdx) in groupRecordListFilter"
size="lg" :key="groupIdx"
class="d-flex align-items-center border-0 p-0 bg-transparent" class="d-flex align-items-center gap-2"
style="width: fit-content;"
> >
<b-form-tag <div class="d-flex flex-wrap align-items-center border rounded p-1 gap-1 flex-wrap">
v-for="(title, i) in activeFilters" <div
:key="i" v-for="(f, filterIndex) in filterGroup.filter"
:title="title" :key="filterIndex"
variant="light" class="active-filter d-flex align-items-center rounded gap-1 pl-2 pr-1 py-1 bg-light"
pill >
class="align-items-center ml-2" <span class="field-label">
@remove="removeFilter(i)" {{ f.label || f.name }}
/> </span>
</b-form-tags>
<span>
{{ $t(`recordList.filter.operatorLabels.${formatActiveFilterOperator(f.operator)}`) }}
</span>
<template v-if="f.value">
<template v-if="isBetweenOperator(f.operator)">
<field-viewer
v-if="f.value.start"
value-only
:field="f.field"
:record="f.record[0]"
:module="recordListModule"
:namespace="namespace"
class="font-weight-bold text-primary"
/>
<span
v-else
class="text-primary font-weight-bold"
>
{{ !f.value.start ? $t('recordList.filter.nil') : '' }}
</span>
<span
class="text-primary text-lowercase"
>
{{ $t('recordList.filter.conditions.and') }}
</span>
<field-viewer
v-if="f.value.end"
value-only
:field="f.field"
:record="f.record[1]"
:module="recordListModule"
:namespace="namespace"
class="font-weight-bold text-primary"
/>
<span
v-else
class="text-primary font-weight-bold"
>
{{ !f.value.end ? $t('recordList.filter.nil') : '' }}
</span>
</template>
<field-viewer
v-else
value-only
:field="f.field"
:record="f.record"
:module="recordListModule"
:namespace="namespace"
class="font-weight-bold text-primary"
/>
</template>
<span
v-else
class="text-primary font-weight-bold"
>
{{ $t('recordList.filter.nil') }}
</span>
<b-button
variant="light"
class="d-flex align-items-center p-1 active-filter-close-btn bg-transparent border-0"
@click="removeFilter(groupIdx, filterIndex) "
>
<font-awesome-icon
:icon="['fas', 'times']"
/>
</b-button>
</div>
</div>
<span
v-if="groupIdx < groupRecordListFilter.length - 1"
class="text-secondary"
>
{{ $t('recordList.filter.conditions.or') }}
</span>
</div>
</div> </div>
<b-button <b-button
@ -751,7 +832,7 @@ import AutomationButtons from './Shared/AutomationButtons'
import { compose, validator, NoID } from '@cortezaproject/corteza-js' import { compose, validator, NoID } from '@cortezaproject/corteza-js'
import users from 'corteza-webapp-compose/src/mixins/users' import users from 'corteza-webapp-compose/src/mixins/users'
import records from 'corteza-webapp-compose/src/mixins/records' import records from 'corteza-webapp-compose/src/mixins/records'
import { evaluatePrefilter, queryToFilter, isFieldInFilter } from 'corteza-webapp-compose/src/lib/record-filter' import { evaluatePrefilter, queryToFilter, isFieldInFilter, formatActiveFilterOperator, isBetweenOperator } from 'corteza-webapp-compose/src/lib/record-filter'
import { getItem, setItem, removeItem } from 'corteza-webapp-compose/src/lib/local-storage' import { getItem, setItem, removeItem } from 'corteza-webapp-compose/src/lib/local-storage'
import { components, url } from '@cortezaproject/corteza-vue' import { components, url } from '@cortezaproject/corteza-vue'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
@ -841,7 +922,6 @@ export default {
ctr: 0, ctr: 0,
items: [], items: [],
showingDeletedRecords: false, showingDeletedRecords: false,
activeFilters: [],
customPresetFilters: [], customPresetFilters: [],
currentCustomPresetFilter: undefined, currentCustomPresetFilter: undefined,
showCustomPresetFilterModal: false, showCustomPresetFilterModal: false,
@ -851,6 +931,8 @@ export default {
recordsPerPage: undefined, recordsPerPage: undefined,
customConfiguredFields: [], customConfiguredFields: [],
formatActiveFilterOperator,
isBetweenOperator,
} }
}, },
@ -1052,6 +1134,53 @@ export default {
isOnRecordPage () { isOnRecordPage () {
return this.page && this.page.moduleID !== NoID return this.page && this.page.moduleID !== NoID
}, },
groupRecordListFilter () {
let groupedData = []
const recordListFilter = this.recordListFilter
for (let i = 0; i < recordListFilter.length; i++) {
const group = this.recordListFilter[i]
const groupFilter = {
filter: group.filter
.map(f => {
return this.createDefaultFilter(f.condition, { name: f.name, kind: f.kind, isMulti: f.isMulti }, f.value, f.operator)
}),
groupCondition: recordListFilter.length && recordListFilter.length - 1 !== i ? 'AND' : undefined,
}
groupFilter.filter = groupFilter.filter.sort((a, b) => a.name.localeCompare(b.name))
const grouped = {}
groupFilter.filter.forEach(filter => {
if (!grouped[filter.name]) {
grouped[filter.name] = []
}
grouped[filter.name].push(filter)
})
groupFilter.filter = []
Object.keys(grouped).forEach((key, index) => {
const group = grouped[key]
group.forEach((filter, idx) => {
if (idx === 0) {
filter.condition = index === 0 ? 'Where' : 'AND'
} else {
filter.condition = 'OR'
}
groupFilter.filter.push(filter)
})
})
groupedData.push(groupFilter)
}
groupedData = groupedData.filter(({ filter }) => filter.length)
return groupedData
},
}, },
watch: { watch: {
@ -1658,7 +1787,7 @@ export default {
this.selected = [] this.selected = []
// Compute query based on query, prefilter and recordListFilter // Compute query based on query, prefilter and recordListFilter
const query = queryToFilter(this.query, this.drillDownFilter || this.prefilter, this.fields.map(({ moduleField }) => moduleField), this.recordListFilter) const query = queryToFilter(this.query, this.drillDownFilter || this.prefilter, this.fields.map(({ moduleField }) => moduleField), this.groupRecordListFilter)
const { moduleID, namespaceID } = this.recordListModule const { moduleID, namespaceID } = this.recordListModule
@ -1750,7 +1879,6 @@ export default {
removeItem(`record-list-filters-${this.uniqueID}`) removeItem(`record-list-filters-${this.uniqueID}`)
} else { } else {
this.recordListFilter = currentFilters this.recordListFilter = currentFilters
this.activeFilters = [...new Set(currentFilters.map(f => f.name).filter(f => !!f))]
} }
} catch (e) { } catch (e) {
// Land here if the filter is corrupted // Land here if the filter is corrupted
@ -1821,12 +1949,67 @@ export default {
this.refresh(true) this.refresh(true)
}, },
setDrillDownFilter (drillDownFilter) { createDefaultFilter (condition, field = {}, value = undefined, operator = undefined) {
if (!this.drillDownFilter) { const fields = [...this.recordListModule.fields, ...this.recordListModule.systemFields()]
this.activeFilters.push(this.$t('recordList.drillDown.filter.label')) const moduleField = (fields.find(({ name }) => name === field.name) || {})
const record = !this.isBetweenOperator(operator)
? { recordID: '0', values: { [moduleField.name]: value } }
: [
{ recordID: '0', values: { [moduleField.name]: value.start } },
{ recordID: '0', values: { [moduleField.name]: value.end } },
]
if (moduleField.isSystem) {
if (!this.isBetweenOperator(operator)) {
record[moduleField.name] = value
} else {
record[0][moduleField.name] = value.start
record[1][moduleField.name] = value.end
}
} }
this.drillDownFilter = drillDownFilter return {
condition,
name: moduleField.name,
operator: operator || (moduleField.isMulti ? 'IN' : '='),
value,
kind: moduleField.kind,
label: moduleField.label || moduleField.name,
field: moduleField,
record: !this.isBetweenOperator(operator)
? new compose.Record(this.recordListModule, { ...record })
: [
new compose.Record(this.recordListModule, { ...record[0] }),
new compose.Record(this.recordListModule, { ...record[1] }),
],
}
},
setDrillDownFilter ({ prefilter: drillDownFilter, name: fieldName, value: fieldValue }) {
if (drillDownFilter) {
const field = (this.recordListModule.fields.find(f => f.name === fieldName) || {})
if (!this.recordListFilter.length) {
this.recordListFilter = [
{
groupCondition: undefined,
filter: [
this.createDefaultFilter('Where', field, fieldValue, true),
],
},
]
} else {
// move to a separate func.
const { filter } = this.recordListFilter[0]
if (!filter.length || (filter.length && !filter[0].name)) {
this.recordListFilter[0].filter = []
this.recordListFilter[0].filter.push(this.createDefaultFilter('Where', field, fieldValue))
} else {
this.recordListFilter[0].filter.push(this.createDefaultFilter('OR', field, fieldValue))
}
}
}
this.pullRecords(true) this.pullRecords(true)
}, },
@ -1929,7 +2112,7 @@ export default {
} }
this.recordListFilter = this.recordListFilter.concat(filter) this.recordListFilter = this.recordListFilter.concat(filter)
this.activeFilters.push(name)
this.refresh(true) this.refresh(true)
if (this.$refs.filterPresets) { if (this.$refs.filterPresets) {
@ -1937,14 +2120,9 @@ export default {
} }
}, },
removeFilter (filterIndex) { removeFilter (groupIndex, filterIndex) {
this.activeFilters.splice(filterIndex, 1) this.recordListFilter = this.groupRecordListFilter
this.recordListFilter[groupIndex].filter = (this.recordListFilter[groupIndex].filter || []).filter((_, index) => index !== filterIndex)
if (this.drillDownFilter && !this.activeFilters.includes(this.$t('recordList.drillDown.filter.label'))) {
this.setDrillDownFilter(undefined)
}
this.recordListFilter = this.recordListFilter.filter(({ name }) => !name || this.activeFilters.includes(name))
this.setStorageRecordListFilter() this.setStorageRecordListFilter()
this.refresh(true) this.refresh(true)
@ -1972,7 +2150,6 @@ export default {
this.ctr = 0 this.ctr = 0
this.items = [] this.items = []
this.showingDeletedRecords = false this.showingDeletedRecords = false
this.activeFilters = []
this.customPresetFilters = [] this.customPresetFilters = []
this.currentCustomPresetFilter = undefined this.currentCustomPresetFilter = undefined
this.showCustomPresetFilterModal = false this.showCustomPresetFilterModal = false
@ -2141,4 +2318,39 @@ td:hover .inline-actions {
.record-list-footer { .record-list-footer {
font-family: var(--font-medium); font-family: var(--font-medium);
} }
.active-filter {
white-space: nowrap;
font-family: var(--font-normal);
.field-label {
font-family: var(--font-medium);
}
&-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
margin: 0;
}
&-item {
vertical-align: middle;
margin: 0;
}
&-close-btn {
vertical-align: middle;
opacity: 0.5;
svg {
height: 0.8rem;
}
&:hover {
opacity: 1;
}
}
}
</style> </style>

View File

@ -218,3 +218,24 @@ export function isFieldInFilter (fieldName, filter = '') {
return filterCases.some(filterCase => filter.includes(filterCase)) return filterCases.some(filterCase => filter.includes(filterCase))
} }
export function formatActiveFilterOperator (op) {
const operators = {
'=': 'equal',
'!=': 'notEqual',
'IN': 'in',
'NOT IN': 'notIn',
'>': 'greaterThan',
'<': 'lessThan',
'LIKE': 'like',
'NOT LIKE': 'notLike',
'BETWEEN': 'between',
'NOT BETWEEN': 'notBetween',
}
return operators[op] || 'is'
}
export function isBetweenOperator (op) {
return ['BETWEEN', 'NOT BETWEEN'].includes(op)
}

View File

@ -37,3 +37,9 @@
} }
} }
} }
.value-only {
.col-form-label {
padding-bottom: 0px !important;
}
}

View File

@ -368,9 +368,22 @@ recordList:
addField: Add new filter field addField: Add new filter field
byValue: Filter records based on field value byValue: Filter records based on field value
addFilterToPreset: Save as preset addFilterToPreset: Save as preset
nil: 'NULL'
conditions: conditions:
and: AND and: AND
or: OR or: OR
operatorLabels:
is: is
equal: is equal
notEqual: is not equal
in: contains
notIn: does not contain
greaterThan: is greater than
lessThan: is less than
like: is like
notLike: not like
between: is between
notBetween: is not between
deletedRecords: Deleted records deletedRecords: Deleted records
field: Filter field field: Filter field
fieldPlaceholder: Pick a field fieldPlaceholder: Pick a field