3
0

Fix record organizer

This commit is contained in:
Jože Fortun
2023-02-06 14:22:26 +01:00
parent 2358d418ba
commit b32f688032
12 changed files with 238 additions and 186 deletions
@@ -88,10 +88,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -191,6 +191,7 @@
<b-thead>
<b-tr :variant="showingDeletedRecords ? 'warning' : ''">
<b-th v-if="options.draggable && inlineEditing" />
<b-th
v-if="options.selectable"
style="width: 0%;"
@@ -203,6 +204,7 @@
@change="handleSelectAllOnPage({ isChecked: $event })"
/>
</b-th>
<b-th v-if="isFederated" />
<b-th
@@ -364,94 +366,93 @@
</b-td>
<b-td
class="d-flex justify-content-end align-items-top"
class="text-right"
@click.stop
>
<template v-if="inlineEditing">
<b-button-group v-if="inlineEditing">
<b-button
v-if="item.r.deletedAt"
variant="link"
size="md"
:title="$t('recordList.record.tooltip.undelete')"
class="border-0 text-dark mt-1 d-print-none"
@click.prevent="handleRestoreInline(item, index)"
>
<font-awesome-icon
:icon="['fa', 'trash-restore']"
/>
</b-button>
<!-- The user should be able to delete the record if it's not yet saved -->
<b-button
v-else-if="item.r.canDeleteRecord && !item.r.deletedAt"
variant="link"
size="md"
class="border-0 show-when-hovered text-danger mt-1 d-print-none"
@click.prevent="handleDeleteInline(item, index)"
>
<font-awesome-icon
:icon="['far', 'trash-alt']"
/>
</b-button>
</template>
<b-button
v-if="!inlineEditing && !options.hideRecordReminderButton"
variant="link"
:title="$t('recordList.record.tooltip.reminder')"
class="p-0 m-0 ml-2 text-primary d-print-none"
@click.prevent="createReminder(item.r)"
>
<font-awesome-icon
:icon="['far', 'bell']"
/>
</b-button>
<template v-if="!options.hideRecordCloneButton && recordListModule.canCreateRecord && (options.rowCreateUrl || recordPageID)">
<b-button
v-if="!inlineEditing"
variant="link"
v-if="showCloneRecordButton"
:title="$t('recordList.record.tooltip.clone')"
class="p-0 m-0 ml-2 text-secondary d-print-none"
:to="{ name: options.rowCreateUrl || 'page.record.create', params: { pageID: recordPageID, values: item.r.values }, query: null }"
>
<font-awesome-icon
:icon="['far', 'clone']"
/>
</b-button>
<b-button
v-else
variant="link"
:title="$t('recordList.record.tooltip.clone')"
class="ml-2 text-primary d-print-none"
variant="outline-light"
class="text-primary d-print-none border-0"
@click="handleCloneInline(item.r)"
>
<font-awesome-icon
:icon="['far', 'clone']"
/>
</b-button>
</template>
<template v-if="!inlineEditing">
<b-button
v-if="item.r.deletedAt"
:title="$t('recordList.record.tooltip.undelete')"
variant="outline-light"
class="border-0 text-dark d-print-none"
@click.prevent="handleRestoreInline(item, index)"
>
<font-awesome-icon
:icon="['fa', 'trash-restore']"
/>
</b-button>
<!-- The user should be able to delete the record if it's not yet saved -->
<b-button
v-else-if="(item.r.canDeleteRecord || item.r.recordID === '0') && !item.r.deletedAt"
variant="outline-light"
class="border-0 show-when-hovered text-danger d-print-none"
@click.prevent="handleDeleteInline(item, index)"
>
<font-awesome-icon
:icon="['far', 'trash-alt']"
/>
</b-button>
</b-button-group>
<b-button-group v-if="!inlineEditing">
<b-button
v-if="!options.hideRecordViewButton && item.r.canReadRecord && (options.rowViewUrl || recordPageID)"
:title="$t('recordList.record.tooltip.view')"
variant="outline-light"
class="text-primary d-print-none border-0"
:to="{ name: options.rowViewUrl || 'page.record', params: { pageID: recordPageID, recordID: item.r.recordID }, query: null }"
>
<font-awesome-icon
:icon="['far', 'eye']"
/>
</b-button>
<b-button
v-if="!options.hideRecordEditButton && item.r.canUpdateRecord && (options.rowEditUrl || recordPageID)"
variant="link"
:title="$t('recordList.record.tooltip.edit')"
class="p-0 m-0 ml-2 text-primary d-print-none"
:to="{ name: options.rowEditUrl || 'page.record.edit', params: { pageID: recordPageID, recordID: item.r.recordID }, query: null }"
variant="outline-light"
class="text-primary d-print-none border-0"
>
<font-awesome-icon
:icon="['far', 'edit']"
/>
</b-button>
<b-button
v-if="!options.hideRecordViewButton && item.r.canReadRecord && (options.rowViewUrl || recordPageID)"
variant="link"
:title="$t('recordList.record.tooltip.view')"
class="p-0 m-0 ml-2 text-primary d-print-none"
:to="{ name: options.rowViewUrl || 'page.record', params: { pageID: recordPageID, recordID: item.r.recordID }, query: null }"
v-if="showCloneRecordButton"
:title="$t('recordList.record.tooltip.clone')"
variant="outline-light"
:to="{ name: options.rowCreateUrl || 'page.record.create', params: { pageID: recordPageID, values: item.r.values }, query: null }"
class="text-primary d-print-none border-0"
>
<font-awesome-icon
:icon="['far', 'eye']"
:icon="['far', 'clone']"
/>
</b-button>
<b-button
v-if="!options.hideRecordReminderButton"
:title="$t('recordList.record.tooltip.reminder')"
variant="outline-light"
class="text-primary d-print-none border-0"
@click.prevent="createReminder(item.r)"
>
<font-awesome-icon
:icon="['far', 'bell']"
/>
</b-button>
@@ -461,10 +462,10 @@
:target="item.r.recordID"
:title="item.r.recordID"
:tooltip="$t('permissions:resources.compose.record.tooltip')"
button-variant="link"
class="text-dark m-0 p-0 ml-2 d-print-none"
button-variant="outline-light"
class="text-dark d-print-none border-0"
/>
</template>
</b-button-group>
</b-td>
</b-tr>
</draggable>
@@ -504,7 +505,7 @@
<div class="text-truncate">
<div
v-if="options.showTotalCount"
class="ml-2 text-nowrap font-weight-bold"
class="ml-2 text-nowrap"
>
<span
v-if="pagination.count > options.perPage"
@@ -522,7 +523,7 @@
</div>
<div
v-if="!options.hidePaging && !inlineEditing"
v-if="showPageNavigation"
>
<b-pagination
v-if="options.fullPageNavigation"
@@ -531,7 +532,6 @@
aria-controls="record-list"
class="m-0 d-print-none"
pills
variant="link"
:value="getPagination.page"
:per-page="getPagination.perPage"
:total-rows="getPagination.count"
@@ -558,8 +558,8 @@
<b-button
:disabled="!hasPrevPage"
data-test-id="first-page"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage()"
>
<font-awesome-icon :icon="['fas', 'angle-double-left']" />
@@ -567,22 +567,28 @@
<b-button
:disabled="!hasPrevPage"
data-test-id="previous-page"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage('prevPage')"
>
<font-awesome-icon :icon="['fas', 'angle-left']" />
<font-awesome-icon
:icon="['fas', 'angle-left']"
class="mr-1"
/>
{{ $t('recordList.pagination.prev') }}
</b-button>
<b-button
:disabled="!hasNextPage"
data-test-id="next-page"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage('nextPage')"
>
{{ $t('recordList.pagination.next') }}
<font-awesome-icon :icon="['fas', 'angle-right']" />
<font-awesome-icon
:icon="['fas', 'angle-right']"
class="ml-1"
/>
</b-button>
</b-button-group>
</div>
@@ -747,6 +753,10 @@ export default {
return this.mode === 'editor'
},
showPageNavigation () {
return this.items.length && !this.options.hidePaging && !this.inlineEditing
},
disableSelectAll () {
if (this.options.hidePaging) {
return !this.items.length
@@ -853,6 +863,10 @@ export default {
return undefined
},
showCloneRecordButton () {
return !this.options.hideRecordCloneButton && this.recordListModule.canCreateRecord && (this.options.rowCreateUrl || this.recordPageID)
},
},
watch: {
@@ -1031,8 +1045,12 @@ export default {
},
handleDeleteInline (item, i) {
const r = new compose.Record(this.recordListModule, { ...item.r, deletedAt: new Date() })
this.items.splice(i, 1, this.wrapRecord(r, item.id))
if (item.r.recordID !== NoID) {
const r = new compose.Record(this.recordListModule, { ...item.r, deletedAt: new Date() })
this.items.splice(i, 1, this.wrapRecord(r, item.id))
} else {
this.items.splice(i, 1)
}
},
handleRestoreInline (item, i) {
@@ -7,7 +7,7 @@
<template #default>
<div
v-if="!isConfigured"
class="p-3 text-danger"
class="d-flex align-items-center justify-content-center h-100 p-2"
>
{{ $t('recordOrganizer.notConfigured') }}
</div>
@@ -19,6 +19,7 @@
v-if="!processing"
:id="draggableID"
v-model="records"
handle=".record-item"
:group="{ name: moduleID, pull: canPull, put: canPut }"
:move="checkMove"
class="h-100 pt-2 px-2 overflow-auto"
@@ -29,7 +30,7 @@
#header
>
<div
class="p-2 text-secondary"
class="small p-2 text-secondary"
>
{{ $t('recordOrganizer.noRecords') }}
</div>
@@ -38,52 +39,58 @@
v-for="record in records"
:key="`${record.recordID}`"
tag="div"
class="mb-2 py-1 px-2 border rounded"
:class="{ 'pointer': roRecordPage, 'grab': canPull && record.canUpdateRecord }"
class="record-item mb-2"
:class="{ 'pointer': roRecordPage }"
:to="{ name: 'page.record', params: { pageID: (roRecordPage || {}).pageID, recordID: record.recordID }, query: null }"
>
<b-card-title
v-if="labelField"
title-tag="h5"
<b-card
body-class="px-2 py-1"
class="border rounded"
>
<field-viewer
v-if="labelField.canReadRecordValue"
:field="labelField"
:record="record"
:namespace="namespace"
value-only
/>
<i
v-else
class="text-secondary h6"
>{{ $t('field.noPermission') }}</i>
</b-card-title>
<b-card-text v-if="descriptionField">
<field-viewer
v-if="descriptionField.canReadRecordValue"
:field="descriptionField"
:record="record"
:namespace="namespace"
value-only
/>
<i
v-else
class="text-primary h6"
<b-card-title
v-if="labelField"
title-tag="div"
class="mb-1"
>
{{ $t('field.noPermission') }}
</i>
</b-card-text>
<field-viewer
v-if="labelField.canReadRecordValue"
:field="labelField"
:record="record"
:namespace="namespace"
value-only
/>
<i
v-else
class="text-secondary"
>{{ $t('field.noPermission') }}</i>
</b-card-title>
<b-card-text
v-if="descriptionField"
class="small"
>
<field-viewer
v-if="descriptionField.canReadRecordValue"
:field="descriptionField"
:record="record"
:namespace="namespace"
value-only
/>
<i
v-else
class="text-secondary"
>
{{ $t('field.noPermission') }}
</i>
</b-card-text>
</b-card>
</router-link>
</draggable>
<h5
<div
v-else
class="d-flex align-items-center justify-content-center w-100 h-100"
class="d-flex align-items-center justify-content-center h-100"
>
<b-spinner
variant="primary"
class="p-4"
/>
</h5>
<b-spinner />
</div>
</div>
</template>
<template
@@ -91,11 +98,11 @@
#footer
>
<div
class="d-flex align-items-center m-0 px-1 py-2"
class="p-2"
>
<b-button
variant="link"
class="text-decoration-none pl-2"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click.prevent="createNewRecord"
>
{{ $t('recordOrganizer.addNewRecord') }}
@@ -111,7 +118,7 @@ import base from './base'
import draggable from 'vuedraggable'
import FieldViewer from 'corteza-webapp-compose/src/components/ModuleFields/Viewer'
import users from 'corteza-webapp-compose/src/mixins/users'
import { evaluatePrefilter } from 'corteza-webapp-compose/src/lib/record-filter'
import { evaluatePrefilter, getFieldFilter } from 'corteza-webapp-compose/src/lib/record-filter'
import { compose, NoID } from '@cortezaproject/corteza-js'
export default {
@@ -224,14 +231,15 @@ export default {
watch: {
options: {
immediate: true,
deep: true,
handler (options = {}) {
if (!options.moduleID) {
// Make sure block is properly configured
throw Error(this.$t('notification:record.moduleOrPageNotSet'))
}
handler () {
this.refresh()
},
},
'record.recordID': {
immediate: true,
handler () {
this.refresh()
},
},
@@ -241,6 +249,14 @@ export default {
this.refreshBlock(this.refresh)
},
mounted () {
this.$root.$on(`refetch-non-record-blocks:${this.page.pageID}`, this.refresh)
},
beforeDestroy () {
this.$root.$off(`refetch-non-record-blocks:${this.page.pageID}`)
},
methods: {
// Allow move if repositioned or if record isn't in target record organizer
checkMove ({ draggedContext = {}, relatedContext = {} }) {
@@ -313,13 +329,16 @@ export default {
// Prefill values with the group value set in the options
const values = {}
if (groupField && group) {
if (groupField) {
values[groupField] = group
}
this.$router.push({ name: 'page.record.create', params: { pageID, values: values } })
this.$router.push({ name: 'page.record.create', params: { pageID, values: values, refRecord: this.record } })
},
expandFilter () {
const filter = []
/* eslint-disable no-template-curly-in-string */
if (!this.record) {
// If there is no current record and we are using recordID/ownerID variable in (pre)filter
@@ -337,15 +356,19 @@ export default {
// Little magic here: filter is wraped with backticks and evaluated
// this allows us to us ${record.values....}, ${recordID}, ${ownerID}, ${userID} in filter string;
// hence the /hanging/ record, recordID, ownerID and userID variables
return evaluatePrefilter(this.options.filter, {
filter.push(`(${evaluatePrefilter(this.options.filter, {
record: this.record,
recordID: (this.record || {}).recordID || NoID,
ownerID: (this.record || {}).ownedBy || NoID,
userID: (this.$auth.user || {}).userID || NoID,
})
})})`)
}
return ''
if (this.groupField && this.options.group !== undefined) {
filter.push(`(${getFieldFilter(this.groupField.name, this.groupField.kind, this.options.group, '=')})`)
}
return filter.join(' AND ')
},
/**
@@ -364,7 +387,7 @@ export default {
const { namespaceID, moduleID, recordID } = record
if (moduleID !== this.options.moduleID) {
throw Error('Record incompatible, module mismatch')
throw Error(this.$t('record.moduleMismatch'))
}
const { positionField, groupField } = this.options
@@ -389,7 +412,7 @@ export default {
args: Object.keys(args).map(name => ({ name, value: String(args[name]) })),
}
return this.$ComposeAPI.recordExec(params)
return this.$ComposeAPI.recordExec(params).then(this.fetchRecords)
},
/**
@@ -399,38 +422,37 @@ export default {
* @param {String} query Filter records
* @returns {Promise<Record[]>}
*/
async fetchRecords (module, query) {
if (module.moduleID !== this.options.moduleID) {
throw Error('Module incompatible, module mismatch')
async fetchRecords () {
if (!this.roModule) {
return
}
if (this.roModule.moduleID !== this.options.moduleID) {
throw Error(this.$t('record.moduleMismatch'))
}
const query = this.expandFilter()
const { labelField, descriptionField, positionField } = this.options
const { moduleID, namespaceID } = module
const { moduleID, namespaceID } = this.roModule
const sort = positionField || `updatedAt, ${labelField || descriptionField}`
this.processing = true
return this.$ComposeAPI.recordList({ namespaceID, moduleID, query, sort })
.then(({ set }) => set.map(r => Object.freeze(new compose.Record(module, r))))
.then(({ set }) => {
this.records = set.map(r => Object.freeze(new compose.Record(this.roModule, r)))
const fields = [this.labelField, this.descriptionField].filter(f => !!f)
this.fetchUsers(fields, this.records)
}).catch(e => {
console.error(e)
}).finally(() => {
this.processing = false
})
},
refresh () {
if (this.roModule) {
this.processing = true
this.fetchRecords(this.roModule, this.expandFilter())
.then(rr => {
this.records = rr
const fields = [this.labelField, this.descriptionField].filter(f => !!f)
this.fetchUsers(fields, this.records)
})
.catch(e => {
console.error(e)
})
.finally(() => {
this.processing = false
})
}
this.fetchRecords()
},
},
}
</script>
@@ -439,4 +461,8 @@ export default {
.grab {
cursor: grab !important;
}
.record-item:hover {
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
}
</style>
@@ -82,7 +82,7 @@
<b-card-footer
v-if="footerSet"
class="p-0 overflow-hidden"
class="p-0 overflow-hidden bg-white border-top"
>
<slot
name="footer"
@@ -1,7 +1,7 @@
<template>
<div class="h-100">
<div
class="d-flex flex-column position-relative h-100 border-0"
class="d-flex flex-column border position-relative h-100 card bg-transparent"
:class="blockClass"
>
<div
@@ -80,7 +80,7 @@
<b-card-footer
v-if="footerSet"
class="card-footer bg-transparent p-0 overflow-hidden"
class="card-footer bg-transparent p-0 overflow-hidden border-top"
>
<slot
name="footer"
@@ -95,10 +95,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -87,10 +87,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -87,10 +87,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -87,10 +87,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -87,10 +87,10 @@ $sidebar-width: 320px !default;
$sidebar-bg: #F4F7FA;
// Pagination
$pagination-color: $dark !default;
$pagination-color: $primary !default;
$pagination-disabled-bg: transparent !default;
$pagination-bg: transparent !default;
$pagination-active-bg: $dark !default;
$pagination-active-bg: $primary !default;
$pagination-border-width: 0 !default;
// Forms
@@ -3,6 +3,8 @@
no-body
class="shadow-sm"
header-bg-variant="white"
footer-bg-variant="white"
footer-class="border-top"
>
<template #header>
<b-container
@@ -128,7 +130,7 @@
>
<div
v-if="!hideTotal"
class="text-nowrap font-weight-bold"
class="text-nowrap"
>
{{ getPagination }}
</div>
@@ -139,29 +141,35 @@
>
<b-button
:disabled="!hasPrevPage"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage()"
>
<font-awesome-icon :icon="['fas', 'angle-double-left']" />
</b-button>
<b-button
:disabled="!hasPrevPage"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage('prevPage')"
>
<font-awesome-icon :icon="['fas', 'angle-left']" />
<font-awesome-icon
:icon="['fas', 'angle-left']"
class="mr-1"
/>
{{ translations.prevPagination }}
</b-button>
<b-button
:disabled="!hasNextPage"
variant="link"
class="text-dark"
variant="outline-light"
class="d-flex align-items-center justify-content-center text-primary border-0"
@click="goToPage('nextPage')"
>
{{ translations.nextPagination }}
<font-awesome-icon :icon="['fas', 'angle-right']" />
<font-awesome-icon
:icon="['fas', 'angle-right']"
class="ml-1"
/>
</b-button>
</b-button-group>
</div>
+2 -2
View File
@@ -406,7 +406,7 @@ recordOrganizer:
footnote: Field value will be used as record description
label: Description field
group:
footnote: Value that will be set to the key field. This does not affect the filtering. Make sure to specify the prefilter where needed.
footnote: Value that will be set to the key field. The records will be filtered based on this value, if it's defined.
label: Key value
groupField:
footnote: Field whose value will change when a record is moved into the record organizer
@@ -418,7 +418,7 @@ recordOrganizer:
noRecords: No records in module linked with record organizer. Drag and drop records here.
notConfigured: Record organizer is not configured correctly
positionField:
footnote: Records will be sorted based on this field
footnote: Records will be sorted based on this field. That field value will be updated based on the position of the record.
label: Record sort field
preview:
label: 'Record Organizer block for module {{0}}. Label field {{1}}, Description field {{2}}. Value setting field: {{3}}, Sorted by position field: {{4}}.'