Add inline editing for record lists

This commit is contained in:
Jože Fortun
2023-03-22 11:51:12 +01:00
parent 0a4ed217c4
commit 363afde876
9 changed files with 253 additions and 69 deletions
@@ -1,10 +1,10 @@
<template>
<div>
<div class="w-100">
<!-- Extra empty line is added thanks to white-space: pre-line (multivalue) if we write div in multiple lines -->
<!-- eslint-disable-next-line -->
<div v-if="field.options.display === 'number'" :class="classes">{{ formatted }}</div>
<div v-else>
<template v-else>
<c-progress
v-for="(v, i) in formatted"
:key="i"
@@ -21,7 +21,7 @@
:class="{ 'mt-2': i }"
style="height: 1.5rem;"
/>
</div>
</template>
<errors :errors="errors" />
</div>
@@ -360,9 +360,10 @@
style="min-width: 150px;"
@click.stop
/>
<div
v-else-if="field.moduleField.canReadRecordValue && !field.edit"
class="mb-0"
class="d-flex mb-0"
:class="{
'field-adjust-offset': inlineEditing,
}"
@@ -374,7 +375,24 @@
:module="module"
:namespace="namespace"
/>
<div
v-if="options.inlineRecordEditEnabled && field.canEdit"
class="inline-actions ml-auto"
>
<b-button
:title="$t('recordList.inlineEdit.button.title')"
variant="outline-light"
size="sm"
class="text-secondary border-0 ml-1"
@click.stop="editInlineField(item.r, field.key)"
>
<font-awesome-icon
:icon="['fas', 'pen']"
/>
</b-button>
</div>
</div>
<i
v-else
class="text-primary"
@@ -384,7 +402,7 @@
</b-td>
<b-td
class="actions px-1"
class="actions px-2"
@click.stop
>
<b-dropdown
@@ -543,6 +561,19 @@
</div>
</b-table-simple>
</div>
<!-- Modal for inline editing -->
<bulk-edit-modal
v-if="options.inlineRecordEditEnabled"
:namespace="namespace"
:module="recordListModule"
:selected-records="inlineEdit.recordIDs"
:selected-fields="inlineEdit.fields"
:initial-record="inlineEdit.record"
:modal-title="$t('recordList.inlineEdit.modal.title')"
open-on-select
@save="onInlineEdit()"
/>
</template>
<template
@@ -756,6 +787,11 @@ export default {
},
selected: [],
inlineEdit: {
fields: [],
recordIDs: [],
initialRecord: {},
},
sortBy: undefined,
sortDirecton: undefined,
@@ -882,6 +918,7 @@ export default {
filterable: mf.isFilterable,
tdClass: 'record-value',
editable: !!editable.find(f => mf.name === f),
canEdit: this.isFieldEditable(mf),
required: this.inlineEditing && mf.isRequired,
}))
@@ -1597,6 +1634,39 @@ export default {
onBulkUpdate () {
this.refresh(true)
},
editInlineField (record, field) {
this.inlineEdit.fields = [field]
this.inlineEdit.record = record.clone()
this.inlineEdit.recordIDs.push(record.recordID)
},
onInlineEdit () {
this.refresh(true)
this.inlineEditRecords = []
},
isFieldEditable (field) {
if (!field) return false
const { canCreateOwnedRecord } = this.recordListModule || {}
const { createdAt, canManageOwnerOnRecord } = this.record || {}
const { name, canUpdateRecordValue, isSystem, expressions = {} } = field || {}
if (!canUpdateRecordValue) return false
if (isSystem) {
// Make ownedBy field editable if correct permissions
if (name === 'ownedBy') {
// If not created we check module permissions, otherwise the canManageOwnerOnRecord
return createdAt ? canManageOwnerOnRecord : canCreateOwnedRecord
}
return false
}
return !expressions.value
},
},
}
</script>
@@ -1624,12 +1694,28 @@ th .required::after {
tr:hover td.actions {
opacity: 1;
background-color: $gray-200;
visibility: visible;
}
.inline-actions {
min-width: 30px;
margin-top: -2px;
opacity: 0;
transition: opacity 0.25s;
}
td:hover .inline-actions {
opacity: 1;
background-color: $gray-200;
button:hover {
color: $primary !important;
}
}
</style>
<style lang="scss">
.record-list-table .actions {
padding-top: 8px;
position: sticky;
right: 0;
opacity: 0;
@@ -501,11 +501,11 @@
md="6"
>
<b-form-group
:label="$t('recordList.enableRecordPageNavigation')"
:label="$t('recordList.inlineEdit.enabled')"
label-class="text-primary"
>
<c-input-checkbox
v-model="options.enableRecordPageNavigation"
v-model="options.inlineRecordEditEnabled"
switch
:labels="checkboxLabel"
/>
@@ -528,6 +528,22 @@
</b-form-group>
</b-col>
<b-col
cols="12"
md="6"
>
<b-form-group
:label="$t('recordList.enableRecordPageNavigation')"
label-class="text-primary"
>
<c-input-checkbox
v-model="options.enableRecordPageNavigation"
switch
:labels="checkboxLabel"
/>
</b-form-group>
</b-col>
<b-col
cols="12"
md="6"
@@ -1,6 +1,7 @@
<template>
<div>
<b-button
v-if="!openOnSelect"
:title="$t('recordList.bulkRecord.title')"
variant="outline-light"
class="text-primary border-0"
@@ -14,66 +15,82 @@
<b-modal
:visible="showModal"
:title="$t('recordList.bulkRecord.title')"
:title="modalTitle || $t('recordList.bulkRecord.title')"
body-class="p-0"
footer-class="d-flex justify-content-between align-items-center"
footer-class="flex-column align-items-stretch"
centered
@hide="onModalHide"
>
<b-card class="pt-0">
<field-editor
<b-card
v-if="fields.length"
class="pt-0"
>
<div
v-for="(field, index) in fields"
:key="index"
:namespace="namespace"
:module="module"
:field="field"
:errors="fieldErrors(field.name)"
:record="record"
/>
<hr
v-if="fields.length"
class="my-4"
:key="field.fieldID"
class="position-relative"
>
<field-editor
:namespace="namespace"
:module="module"
:field="getField(field)"
:errors="fieldErrors(field)"
:record="record"
/>
<c-input-confirm
class="position-absolute"
:tooltip="$t('recordList.bulkRecord.field.remove')"
style="top: -2px; right: -4.5px; z-index: 2;"
@confirmed="fields.splice(index, 1)"
/>
</div>
</b-card>
<template #modal-footer>
<vue-select
v-model="selectedField"
:placeholder="$t('recordList.bulkRecord.searchFields')"
:placeholder="getFieldSelectorPlaceholder"
:get-option-label="getFieldLabel"
:options="moduleFields"
append-to-body
:calculate-position="calculatePosition"
:selectable="option => !selectedFields.includes(option.name)"
:selectable="option => !fields.includes(option.name)"
:reduce="f => f.name"
class="bg-white position-relative"
@input="addField"
/>
</b-card>
<template #modal-footer>
<b-button
variant="light"
:disabled="processing"
@click="onReset"
<hr class="my-3">
<div
class="d-flex justify-content-between align-items-center"
>
{{ $t('recordList.bulkRecord.reset') }}
</b-button>
<b-button
variant="light"
:disabled="processing"
@click="onReset"
>
{{ $t('recordList.bulkRecord.reset') }}
</b-button>
<div>
<b-button
variant="link"
rounded
class="text-decoration-none text-primary"
@click="onModalHide"
>
{{ $t('general.label.cancel') }}
</b-button>
<b-button
variant="primary"
:disabled="!fields.length || processing"
@click="handleBulkUpdateSelectedRecords(selectedRecords)"
>
{{ $t('general.label.save') }}
</b-button>
<div>
<b-button
variant="link"
rounded
class="text-decoration-none text-primary"
@click="onModalHide"
>
{{ $t('general.label.cancel') }}
</b-button>
<b-button
variant="primary"
:disabled="!fields.length || processing"
@click="handleBulkUpdateSelectedRecords(selectedRecords)"
>
{{ $t('general.label.save') }}
</b-button>
</div>
</div>
</template>
</b-modal>
@@ -109,6 +126,7 @@ export default {
type: compose.Namespace,
required: true,
},
module: {
type: compose.Module,
required: true,
@@ -118,6 +136,26 @@ export default {
type: Array,
required: true,
},
selectedFields: {
type: Array,
default: () => ([]),
},
initialRecord: {
type: Object,
default: () => ({}),
},
openOnSelect: {
type: Boolean,
default: false,
},
modalTitle: {
type: String,
default: '',
},
},
data () {
@@ -129,10 +167,6 @@ export default {
},
computed: {
selectedFields () {
return this.fields.map(({ name }) => name)
},
moduleFields () {
return [
...[...this.module.fields].sort((a, b) =>
@@ -141,6 +175,32 @@ export default {
...this.module.systemFields().filter(({ name }) => name === 'ownedBy'),
].filter((field) => this.isFieldEditable(field))
},
getFieldSelectorPlaceholder () {
return this.$t(`recordList.bulkRecord.field.add${this.fields.length ? 'Another' : ''}`)
},
},
watch: {
selectedRecords: {
handler (records) {
if (!this.openOnSelect || !records.length) return
this.record = new compose.Record(this.module, this.initialRecord)
this.showModal = true
},
},
selectedFields: {
handler (fields = []) {
if (!fields.length) return
fields.forEach(f => {
if (this.fields.includes(f)) return
this.fields.push(f)
})
},
},
},
created () {
@@ -150,6 +210,10 @@ export default {
methods: {
onModalHide () {
this.showModal = false
if (this.openOnSelect) {
this.fields = []
}
},
getFieldLabel ({ kind, label, name }) {
@@ -164,10 +228,18 @@ export default {
},
onReset () {
this.record = new compose.Record(this.module, {})
this.record = new compose.Record(this.module, this.initialRecord)
this.fields = []
},
getField (fieldName) {
const field = this.moduleFields.find(
({ name }) => name === fieldName,
)
return field || {}
},
isFieldEditable (field) {
if (!field) return false
+4 -3
View File
@@ -265,10 +265,9 @@ export default {
handleBulkUpdateSelectedRecords: throttle(function (records) {
this.processing = true
const { moduleID, namespaceID } = this.module
const values = []
this.fields.forEach(({ name, isMulti }) => {
this.fields.forEach(f => {
const { name, isMulti } = this.getField(f)
const value = this.record.values[name] || this.record[name]
if (!isMulti) {
@@ -280,6 +279,8 @@ export default {
}
})
const { moduleID, namespaceID } = this.module
return this
.$ComposeAPI.recordPatch({ moduleID, namespaceID, records, values })
.catch(err => {
@@ -127,6 +127,7 @@ export default {
presort: 'createdAt DESC',
enableRecordPageNavigation: true,
hideConfigureFieldsButton: false,
inlineRecordEditEnabled: true,
},
})
@@ -57,6 +57,7 @@ interface Options {
selectionButtons: Array<Button>;
bulkRecordEditEnabled: boolean;
inlineRecordEditEnabled: boolean;
}
const defaults: Readonly<Options> = Object.freeze({
@@ -72,11 +73,11 @@ const defaults: Readonly<Options> = Object.freeze({
hidePaging: false,
hideSorting: false,
hideFiltering: false,
hideRecordReminderButton: true,
hideRecordCloneButton: true,
hideRecordReminderButton: false,
hideRecordCloneButton: false,
hideRecordEditButton: false,
hideRecordViewButton: true,
hideRecordPermissionsButton: true,
hideRecordViewButton: false,
hideRecordPermissionsButton: false,
enableRecordPageNavigation: false,
allowExport: false,
perPage: 20,
@@ -104,7 +105,8 @@ const defaults: Readonly<Options> = Object.freeze({
refreshRate: 0,
showRefresh: false,
bulkRecordEditEnabled: true
bulkRecordEditEnabled: true,
inlineRecordEditEnabled: false
})
export class PageBlockRecordList extends PageBlock {
@@ -160,7 +162,8 @@ export class PageBlockRecordList extends PageBlock {
'draggable',
'linkToParent',
'showRefresh',
'bulkRecordEditEnabled'
'bulkRecordEditEnabled',
'inlineRecordEditEnabled'
)
if (o.selectionButtons) {
+1 -1
View File
@@ -116,7 +116,7 @@ export class Record {
}
clone (): Record {
return new Record(JSON.parse(JSON.stringify(this)))
return new Record(this.module, JSON.parse(JSON.stringify(this)))
}
/**
+10 -5
View File
@@ -240,13 +240,18 @@ record:
recordList:
addRecord: Add
cancelSelection: Cancel
inlineEdit:
enabled: Inline value editing enabled
button:
title: Edit value
modal:
title: Update record values
bulkRecord:
title: Update selected records
field: Field
value: Value
type: Type
add: Add
searchFields: Select field to update
field:
add: Add a field to update
addAnother: Add another field to update
remove: Remove field
reset: Reset
drillDown:
filter: