Add inline editing for record lists
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user