diff --git a/client/web/compose/src/components/Chart/index.vue b/client/web/compose/src/components/Chart/index.vue index b727f79b4..496a9c62e 100644 --- a/client/web/compose/src/components/Chart/index.vue +++ b/client/web/compose/src/components/Chart/index.vue @@ -11,6 +11,7 @@ v-if="renderer" :chart="renderer" class="flex-fill p-1" + v-on="$listeners" /> diff --git a/client/web/compose/src/components/PageBlocks/ChartBase.vue b/client/web/compose/src/components/PageBlocks/ChartBase.vue index a7f741aee..4e66c152c 100644 --- a/client/web/compose/src/components/PageBlocks/ChartBase.vue +++ b/client/web/compose/src/components/PageBlocks/ChartBase.vue @@ -10,6 +10,7 @@ :chart="chart" :record="record" :reporter="reporter" + @click="drillDown" /> @@ -17,7 +18,7 @@ import { mapActions } from 'vuex' import base from './base' import ChartComponent from '../Chart' -import { NoID } from '@cortezaproject/corteza-js' +import { NoID, compose } from '@cortezaproject/corteza-js' import { evaluatePrefilter } from 'corteza-webapp-compose/src/lib/record-filter' export default { @@ -33,12 +34,18 @@ export default { data () { return { chart: null, + + filter: undefined, + + drillDownFilter: undefined, } }, mounted () { this.fetchChart() this.refreshBlock(this.refresh) + + this.$root.$on('drill-down-chart', this.drillDown) }, methods: { @@ -49,7 +56,7 @@ export default { async fetchChart (params = {}) { const { chartID } = this.options - if (chartID === NoID) { + if (!chartID) { return } @@ -61,15 +68,18 @@ export default { }, reporter (r) { - const nr = { ...r } - if (nr.filter) { + this.filter = r + + let filter = r.filter + + if (filter) { // If we use ${record} or ${ownerID} and there is no record, resolve empty /* eslint-disable no-template-curly-in-string */ - if (!this.record && (nr.filter.includes('${record') || nr.filter.includes('${ownerID}'))) { + if (!this.record && (filter.includes('${record') || filter.includes('${ownerID}'))) { return new Promise((resolve) => resolve([])) } - nr.filter = evaluatePrefilter(nr.filter, { + filter = evaluatePrefilter(filter, { record: this.record, recordID: (this.record || {}).recordID || NoID, ownerID: (this.record || {}).ownedBy || NoID, @@ -78,7 +88,8 @@ export default { } const { namespaceID } = this.namespace - return this.$ComposeAPI.recordReport({ namespaceID, ...nr }) + + return this.$ComposeAPI.recordReport({ namespaceID, ...r, filter }) }, refresh () { @@ -87,6 +98,60 @@ export default { this.key++ }) }, + + /** + * + * @param {*} name + * Based on drill down configuration, either changes the linked block on the page + * or opens it in a modal wit the filter and dimensions from the chart and the clicked value + */ + drillDown ({ name }) { + const { chartID, drillDown } = this.options + + if (!drillDown.enabled) { + return + } + + // Get recordListID that is linked + let { moduleID, dimensions, filter } = this.filter + + // Construct filter + const dimensionFilter = dimensions ? `(${dimensions} = '${name}')` : '' + filter = filter ? `(${filter})` : '' + const prefilter = [dimensionFilter, filter].filter(f => f).join(' AND ') + + if (drillDown.blockID) { + // Use linked record list to display drill down data + const { pageID = NoID } = this.page + const { recordID = NoID } = this.record || {} + // Construct its uniqueID to identify it + const recordListUniqueID = [pageID, recordID, drillDown.blockID].map(v => v || NoID).join('-') + + this.$root.$emit(`drill-down-recordList:${recordListUniqueID}`, prefilter) + } else { + // Open in modal + const block = new compose.PageBlockRecordList({ + title: `${dimensions} = '${name}'`, + blockID: `drillDown-${chartID}`, + options: { + moduleID, + prefilter, + presort: 'createdAt DESC', + hideRecordReminderButton: true, + hideRecordViewButton: false, + hideConfigureFieldsButton: false, + hideImportButton: true, + selectable: true, + allowExport: true, + perPage: 14, + showTotalCount: true, + magnifyOption: 'modal', + }, + }) + + this.$root.$emit('magnify-page-block', { block }) + } + }, }, } diff --git a/client/web/compose/src/components/PageBlocks/ChartConfigurator.vue b/client/web/compose/src/components/PageBlocks/ChartConfigurator.vue index 3757abf63..f4d69a9ef 100644 --- a/client/web/compose/src/components/PageBlocks/ChartConfigurator.vue +++ b/client/web/compose/src/components/PageBlocks/ChartConfigurator.vue @@ -3,28 +3,68 @@ - + + + + + + + + - - {{ $t('chart.openInBuilder') }} - + + + + {{ $t('chart.drillDown.label') }} + + + + + + + + diff --git a/client/web/compose/src/components/PageBlocks/RecordListBase.vue b/client/web/compose/src/components/PageBlocks/RecordListBase.vue index 1f70cf2d6..6190100f9 100644 --- a/client/web/compose/src/components/PageBlocks/RecordListBase.vue +++ b/client/web/compose/src/components/PageBlocks/RecordListBase.vue @@ -78,7 +78,7 @@ /> - - + {{ $t('recordList.drillDown.filter.remove') }} + + + + + {{ $t('recordList.selected', { count: selected.length, total: items.length }) }} - ({{ $t('recordList.cancelSelection') }}) - - - + + + + - - + + @@ -665,7 +677,9 @@ export default { processing: false, // prefilter from block config - prefilter: null, + prefilter: undefined, + recordListFilter: [], + drillDownFilter: undefined, // raw query string used to build final filter query: null, @@ -696,8 +710,6 @@ export default { // component ctr: 0, items: [], - idPrefix: `rl:${this.blockIndex}`, - recordListFilter: [], showingDeletedRecords: false, } }, @@ -884,23 +896,8 @@ export default { 'record.recordID': { immediate: true, - handler (recordID = NoID) { - const { pageID = NoID } = this.page - - // Set uniqueID so that events dont mix - if (this.uniqueID) { - this.$root.$off(`record-line:collect:${this.uniqueID}`) - this.$root.$off(`page-block:validate:${this.uniqueID}`) - this.$root.$off(`refetch-non-record-blocks:${pageID}`) - } - - this.uniqueID = [pageID, recordID, this.blockIndex].map(v => v || NoID).join('-') - this.$root.$on(`record-line:collect:${this.uniqueID}`, this.resolveRecords) - this.$root.$on(`page-block:validate:${this.uniqueID}`, this.validatePageBlock) - this.$root.$on(`refetch-non-record-blocks:${pageID}`, () => { - this.refresh(true) - }) - + handler () { + this.createEvents() this.getStorageRecordListFilter() this.prepRecordList() this.refresh(true) @@ -908,10 +905,12 @@ export default { }, }, + mounted () { + this.createEvents() + }, + beforeDestroy () { - this.$root.$off(`record-line:collect:${this.uniqueID}`) - this.$root.$off(`page-block:validate:${this.uniqueID}`) - this.$root.$off(`refetch-non-record-blocks:${this.page.pageID}`) + this.destroyEvents() }, created () { @@ -921,6 +920,34 @@ export default { }, methods: { + createEvents () { + const { pageID = NoID } = this.page + const { recordID = NoID } = this.record || {} + + // Set uniqueID so that events dont mix + if (this.uniqueID) { + this.$root.$off(`record-line:collect:${this.uniqueID}`) + this.$root.$off(`page-block:validate:${this.uniqueID}`) + this.$root.$off(`drill-down-recordList:${this.uniqueID}`) + this.$root.$off(`refetch-non-record-blocks:${pageID}`) + } + + this.uniqueID = [pageID, recordID, this.block.blockID].map(v => v || NoID).join('-') + this.$root.$on(`record-line:collect:${this.uniqueID}`, this.resolveRecords) + this.$root.$on(`page-block:validate:${this.uniqueID}`, this.validatePageBlock) + this.$root.$on(`drill-down-recordList:${this.uniqueID}`, this.setDrillDownFilter) + this.$root.$on(`refetch-non-record-blocks:${pageID}`, () => { + this.refresh(true) + }) + }, + + destroyEvents () { + this.$root.$off(`record-line:collect:${this.uniqueID}`) + this.$root.$off(`page-block:validate:${this.uniqueID}`) + this.$root.$off(`refetch-non-record-blocks:${this.page.pageID}`) + this.$root.$off(`drill-down-recordList:${this.uniqueID}`) + }, + onFilter (filter = []) { this.recordListFilter = filter this.setStorageRecordListFilter() @@ -989,7 +1016,7 @@ export default { return { r, - id: id || (r.recordID !== NoID ? r.recordID : `${this.idPrefix}:${this.ctr++}`), + id: id || (r.recordID !== NoID ? r.recordID : `${this.uniqueID}:${this.ctr++}`), } }, @@ -1022,7 +1049,7 @@ export default { module: this.recordListModule, refField: this.options.refField, positionField: this.options.positionField, - idPrefix: this.idPrefix, + idPrefix: this.uniqueID, }) }, @@ -1350,7 +1377,7 @@ export default { this.selected = [] // Compute query based on query, prefilter and recordListFilter - const query = queryToFilter(this.query, this.prefilter, this.recordListModule.filterFields(this.options.fields), this.recordListFilter) + const query = queryToFilter(this.query, this.drillDownFilter || this.prefilter, this.recordListModule.filterFields(this.options.fields), this.recordListFilter) const { moduleID, namespaceID } = this.recordListModule if (this.filter.pageCursor) { @@ -1455,6 +1482,11 @@ export default { onImportSuccessful () { this.refresh(true) }, + + setDrillDownFilter (drillDownFilter) { + this.drillDownFilter = drillDownFilter + this.pullRecords(true) + }, }, } diff --git a/client/web/compose/src/components/PageBlocks/Wrap/Card.vue b/client/web/compose/src/components/PageBlocks/Wrap/Card.vue index 44af43734..73e7b96d9 100644 --- a/client/web/compose/src/components/PageBlocks/Wrap/Card.vue +++ b/client/web/compose/src/components/PageBlocks/Wrap/Card.vue @@ -43,7 +43,7 @@ :title="$t('general.label.magnify')" variant="outline-light" class="text-secondary d-print-none border-0" - @click="$root.$emit('magnify-page-block', isBlockOpened ? undefined : block.blockID)" + @click="$root.$emit('magnify-page-block', isBlockOpened ? undefined : { blockID: block.blockID })" > diff --git a/client/web/compose/src/components/PageBlocks/Wrap/Plain.vue b/client/web/compose/src/components/PageBlocks/Wrap/Plain.vue index 7c39dc9eb..dbdaf5a70 100644 --- a/client/web/compose/src/components/PageBlocks/Wrap/Plain.vue +++ b/client/web/compose/src/components/PageBlocks/Wrap/Plain.vue @@ -41,7 +41,7 @@ :title="$t('general.label.magnify')" variant="outline-light" class="d-print-none border-0" - @click="$root.$emit('magnify-page-block', isBlockOpened ? undefined : block.blockID)" + @click="$root.$emit('magnify-page-block', isBlockOpened ? undefined : { blockID: block.blockID })" > diff --git a/client/web/compose/src/components/Public/Page/Block/Modal.vue b/client/web/compose/src/components/Public/Page/Block/Modal.vue index 3d1019fa6..d35b67c61 100644 --- a/client/web/compose/src/components/Public/Page/Block/Modal.vue +++ b/client/web/compose/src/components/Public/Page/Block/Modal.vue @@ -51,6 +51,10 @@ export default { block: undefined, record: undefined, page: undefined, + + // Used if you want to display a specific block in the modal + // Otherwise its retrieved based on the page and blockID + customBlock: undefined, } }, @@ -95,7 +99,9 @@ export default { }, created () { - this.$root.$on('magnify-page-block', blockID => { + this.$root.$on('magnify-page-block', ({ blockID, block } = {}) => { + this.customBlock = block + blockID = blockID || (block || {}).blockID this.$router.push({ query: { ...this.$route.query, blockID } }) }) }, @@ -117,7 +123,7 @@ export default { return } - this.block = this.page.blocks.find(block => block.blockID === blockID) + this.block = this.customBlock || this.page.blocks.find(block => block.blockID === blockID) const { namespaceID, moduleID } = this.page const recordID = paramsRecordID || queryRecordID diff --git a/client/web/compose/src/views/Admin/Modules/Records/List.vue b/client/web/compose/src/views/Admin/Modules/Records/List.vue index e493648f1..0053f2362 100644 --- a/client/web/compose/src/views/Admin/Modules/Records/List.vue +++ b/client/web/compose/src/views/Admin/Modules/Records/List.vue @@ -123,17 +123,14 @@ export default { fullPageNavigation: true, showTotalCount: true, presort: 'createdAt DESC', + // Set allrecords configuration + allRecords: true, + hideConfigureFieldsButton: false, + rowViewUrl: 'admin.modules.record.view', + rowEditUrl: 'admin.modules.record.edit', + rowCreateUrl: 'admin.modules.record.create', }, }) - - // Set allrecords configuration - this.block.options = { - ...this.block.options, - allRecords: true, - rowViewUrl: 'admin.modules.record.view', - rowEditUrl: 'admin.modules.record.edit', - rowCreateUrl: 'admin.modules.record.create', - } }, methods: { diff --git a/lib/js/src/compose/types/page-block/chart.ts b/lib/js/src/compose/types/page-block/chart.ts index 59b5df37a..0b6e4dc8c 100644 --- a/lib/js/src/compose/types/page-block/chart.ts +++ b/lib/js/src/compose/types/page-block/chart.ts @@ -3,18 +3,28 @@ import { Apply, CortezaID, NoID } from '../../../cast' const kind = 'Chart' +interface DrillDown { + enabled: boolean; + blockID?: string; +} + interface Options { chartID: string; refreshRate: number; showRefresh: boolean; magnifyOption: string; + drillDown: DrillDown; } const defaults: Readonly = Object.freeze({ - chartID: NoID, + chartID: '', refreshRate: 0, - magnifyOption: '', showRefresh: false, + magnifyOption: '', + drillDown: { + enabled: false, + blockID: '' + } }) export class PageBlockChart extends PageBlock { @@ -30,10 +40,15 @@ export class PageBlockChart extends PageBlock { applyOptions (o?: Partial): void { if (!o) return - Apply(this.options, o, CortezaID, 'chartID') + o.chartID = o.chartID === NoID ? '' : o.chartID + + Apply(this.options, o, String, 'chartID', 'magnifyOption') Apply(this.options, o, Number, 'refreshRate') Apply(this.options, o, Boolean, 'showRefresh') - Apply(this.options, o, String, 'magnifyOption') + + if (o.drillDown) { + this.options.drillDown = o.drillDown + } } } diff --git a/lib/js/src/compose/types/page-block/record-list.ts b/lib/js/src/compose/types/page-block/record-list.ts index 3c0edc3e9..d20ef56d6 100644 --- a/lib/js/src/compose/types/page-block/record-list.ts +++ b/lib/js/src/compose/types/page-block/record-list.ts @@ -13,6 +13,7 @@ interface Options { hideHeader: boolean; hideAddButton: boolean; hideImportButton: boolean; + hideConfigureFieldsButton: boolean; hideSearch: boolean; hidePaging: boolean; hideSorting: boolean; @@ -62,6 +63,7 @@ const defaults: Readonly = Object.freeze({ hideHeader: false, hideAddButton: false, hideImportButton: false, + hideConfigureFieldsButton: true, hideSearch: false, hidePaging: false, hideSorting: false, @@ -130,6 +132,7 @@ export class PageBlockRecordList extends PageBlock { 'hideHeader', 'hideAddButton', 'hideImportButton', + 'hideConfigureFieldsButton', 'hideSearch', 'hidePaging', 'fullPageNavigation', diff --git a/lib/vue/src/components/chart/CChart.vue b/lib/vue/src/components/chart/CChart.vue index a49407e3f..6020833cd 100644 --- a/lib/vue/src/components/chart/CChart.vue +++ b/lib/vue/src/components/chart/CChart.vue @@ -4,6 +4,7 @@ :theme="theme" autoresize class="position-absolute w-100 h-100 overflow-hidden" + v-on="$listeners" /> diff --git a/locale/en/corteza-webapp-compose/block.yaml b/locale/en/corteza-webapp-compose/block.yaml index e361ede88..db081e485 100644 --- a/locale/en/corteza-webapp-compose/block.yaml +++ b/locale/en/corteza-webapp-compose/block.yaml @@ -80,7 +80,11 @@ chart: label: Configure chart reportLabel: Report {{l}} reportsLabel: Reports - display: 'Chart to display inside this block:' + display: Chart to display inside this block + drillDown: + label: Drill down + description: If a record list is selected, it will be used to display the drill down data. If no record list is selected the drill down data will be shown in a modal. + openInModal: Open in modal edit: label: Edit chart dimension: @@ -224,6 +228,9 @@ record: recordList: addRecord: Add cancelSelection: Cancel + drillDown: + filter: + remove: Remove drill down filter tooltip: deleteSelected: Delete selected records undeleteSelected: Undelete selected records