Add inline editing to record block

This commit is contained in:
Kelani Tolulope
2023-07-10 11:36:17 +01:00
parent 1e82fc034a
commit 0b9ccdb505
16 changed files with 288 additions and 213 deletions
@@ -2,7 +2,7 @@
<div
v-if="text"
:id="id"
class="ml-1"
class="d-flex align-items-center ml-2"
>
<font-awesome-icon
:icon="['far', 'question-circle']"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
v-slot="ctx"
@@ -1,6 +1,6 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:state="state"
:class="formGroupStyleClasses"
>
@@ -8,28 +8,23 @@
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
v-slot="ctx"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<uploader
:endpoint="endpoint"
:accepted-files="mimetypes"
@@ -1,6 +1,6 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:state="state"
:class="formGroupStyleClasses"
>
@@ -8,28 +8,23 @@
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<div class="d-flex w-100">
<b-button
v-if="field.isMulti"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
v-slot="ctx"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
:value.sync="value"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<template v-if="field.isMulti">
<multi
:value.sync="value"
@@ -1,6 +1,6 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:state="state"
:class="formGroupStyleClasses"
>
@@ -8,28 +8,23 @@
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
v-slot="ctx"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="form-text font-weight-light text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
v-slot="ctx"
@@ -1,34 +1,29 @@
<template>
<b-form-group
label-class="text-primary"
label-class="d-flex align-items-center text-primary p-0"
:class="formGroupStyleClasses"
>
<template
v-if="!valueOnly"
#label
>
<div
class="d-flex align-items-top"
>
<label
class="mb-0"
>
{{ label }}
</label>
<span class="d-inline-block text-truncate mw-100 py-1">
{{ label }}
</span>
<hint
:id="field.fieldID"
:text="hint"
/>
</div>
<small
class="text-muted"
>
{{ description }}
</small>
<hint
:id="field.fieldID"
:text="hint"
/>
</template>
<div
class="small text-muted"
:class="{ 'mb-1': description }"
>
{{ description }}
</div>
<multi
v-if="field.isMulti"
:value.sync="value"
@@ -17,31 +17,52 @@
<div
v-for="(field, index) in fields"
:key="index"
:class="{ 'd-flex flex-column mb-3 px-3': canDisplay(field) }"
:class="{ 'd-flex flex-column mb-3 px-3 field-container': canDisplay(field) }"
>
<template
v-if="canDisplay(field)"
>
<label
class="text-primary mb-0"
:class="{ 'mb-0': !!(field.options.description || {}).view || false }"
class="d-flex align-items-center text-primary mb-0"
>
{{ field.label || field.name }}
<span class="d-inline-block text-truncate mw-100 py-1">
{{ field.label || field.name }}
</span>
<hint
:id="field.fieldID"
:text="((field.options.hint || {}).view || '')"
class="d-inline-block"
/>
<div
v-if="options.inlineRecordEditEnabled && isFieldEditable(field)"
class="inline-actions ml-2"
>
<b-button
:title="$t('field.inlineEdit.button.title')"
variant="outline-light"
size="sm"
:disabled="editable"
class="text-secondary border-0"
@click="editInlineField(fieldRecord, field)"
>
<font-awesome-icon
:icon="['fas', 'pen']"
/>
</b-button>
</div>
</label>
<small
class="text-muted"
<div
class="small text-muted"
:class="{ 'mb-1': !!(field.options.description || {}).view }"
>
{{ (field.options.description || {}).view }}
</small>
</div>
<div
v-if="field.canReadRecordValue"
class="value mt-2"
class="value mt-1"
>
<field-viewer
v-bind="{ ...$props, field }"
@@ -58,6 +79,19 @@
</template>
</div>
</div>
<!-- Modal for inline editing -->
<bulk-edit-modal
:namespace="namespace"
:module="fieldModule"
:selected-records="inlineEdit.recordIDs"
:selected-fields="inlineEdit.fields"
:initial-record="inlineEdit.record"
:query="inlineEdit.query"
:modal-title="$t('field.inlineEdit.modal.title')"
open-on-select
@save="onInlineEdit()"
/>
</wrap>
</template>
<script>
@@ -65,6 +99,7 @@ import { compose, NoID } from '@cortezaproject/corteza-js'
import { mapActions } from 'vuex'
import base from './base'
import FieldViewer from 'corteza-webapp-compose/src/components/ModuleFields/Viewer'
import BulkEditModal from 'corteza-webapp-compose/src/components/Public/Record/BulkEdit'
import Hint from 'corteza-webapp-compose/src/components/Common/Hint.vue'
import users from 'corteza-webapp-compose/src/mixins/users'
import records from 'corteza-webapp-compose/src/mixins/records'
@@ -78,6 +113,7 @@ export default {
components: {
FieldViewer,
Hint,
BulkEditModal,
},
extends: base,
@@ -92,6 +128,11 @@ export default {
return {
referenceRecord: undefined,
referenceModule: undefined,
inlineEdit: {
fields: [],
recordIDs: [],
initialRecord: {},
},
}
},
@@ -216,6 +257,44 @@ export default {
this.toastErrorHandler(this.$t('notification:record.loadFailed'))(e)
})
},
editInlineField (record, field) {
this.inlineEdit.fields = [field.name]
this.inlineEdit.record = record.clone()
this.inlineEdit.recordIDs = [record.recordID]
this.inlineEdit.query = `recordID = ${record.recordID}`
},
onInlineEdit () {
this.inlineEdit.fields = []
this.inlineEdit.recordIDs = []
this.inlineEdit.record = {}
this.inlineEdit.query = ''
this.$root.$emit('refetch-record-blocks')
},
isFieldEditable (field) {
if (!field) return false
const { canCreateOwnedRecord } = this.fieldModule || {}
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>
@@ -224,3 +303,18 @@ export default {
min-height: 1.2rem;
}
</style>
<style lang="scss" scoped>
.inline-actions {
opacity: 0;
transition: opacity 0.25s;
}
.field-container:hover .inline-actions {
opacity: 1;
button:hover {
color: $primary !important;
}
}
</style>
@@ -64,6 +64,22 @@
/>
</b-form-group>
</b-col>
<b-col
cols="12"
md="6"
>
<b-form-group
:label="$t('record.inlineEdit.enabled')"
label-class="text-primary"
>
<c-input-checkbox
v-model="options.inlineRecordEditEnabled"
switch
:labels="checkboxLabel"
/>
</b-form-group>
</b-col>
</b-row>
</div>
@@ -219,6 +235,10 @@ export default {
data () {
return {
referenceModule: undefined,
checkboxLabel: {
on: this.$t('general:label.yes'),
off: this.$t('general:label.no'),
},
}
},
@@ -13,12 +13,12 @@
<div
v-else-if="module"
class="mt-3 px-3"
class="mt-3"
>
<div
v-for="field in fields"
:key="field.id"
:class="{ 'd-flex flex-column': canDisplay(field) }"
:class="{ 'd-flex flex-column mb-3 px-3': canDisplay(field) }"
>
<template
v-if="canDisplay(field)"
@@ -15,6 +15,7 @@ interface Options {
recordSelectorDisplayOption: string;
referenceField?: string;
referenceModuleID?: string;
inlineRecordEditEnabled: boolean;
}
const defaults: Readonly<Options> = Object.freeze({
@@ -23,7 +24,8 @@ const defaults: Readonly<Options> = Object.freeze({
magnifyOption: '',
recordSelectorDisplayOption: 'sameTab',
referenceField: '',
referenceModuleID: undefined
referenceModuleID: undefined,
inlineRecordEditEnabled: false,
})
export class PageBlockRecord extends PageBlock {
@@ -40,6 +42,7 @@ export class PageBlockRecord extends PageBlock {
if (!o) return
Apply(this.options, o, String, 'magnifyOption', 'recordSelectorDisplayOption', 'referenceField', 'referenceModuleID')
Apply(this.options, o, Boolean, 'inlineRecordEditEnabled')
if (o.fields) {
this.options.fields = o.fields
@@ -245,6 +245,8 @@ record:
listLoadFailed: Could not load record list
moduleOrPageNotSet: 'RecordList block error: module or page option not set'
moduleMismatch: Module incompatible, module mismatch
inlineEdit:
enabled: Inline value editing enabled
preview:
blockNoRecord: Cannot render this block without a record
fieldsFromModule: Single record block, displaying fields ({{0}}) from module {{1}}
@@ -580,6 +582,12 @@ field:
selector:
available: Available fields
systemField: (system field)
inlineEdit:
enabled: Inline value editing enabled
button:
title: Edit value
modal:
title: Update record values
comment:
label: 'Comments'
titleField: