3
0

Add page layout record toolbar and actions

This commit is contained in:
Jože Fortun
2023-03-30 21:52:05 +02:00
parent b14b9c5c22
commit 5aaf90f40d
5 changed files with 289 additions and 35 deletions

View File

@@ -13,7 +13,7 @@
class="d-flex align-items-center justify-content-start"
>
<b-button
v-if="!settings.hideBack"
v-if="!(hideBack || settings.hideBack)"
data-test-id="button-back"
variant="link"
class="text-dark back"
@@ -64,7 +64,7 @@
v-if="module"
>
<c-input-confirm
v-if="(isCreated && !settings.hideDelete && !isDeleted)"
v-if="isCreated && !(isDeleted || hideDelete || settings.hideDelete)"
:disabled="!canDeleteRecord"
size="lg"
size-confirm="lg"
@@ -105,7 +105,7 @@
</c-input-confirm>
<b-button
v-if="!inEditing && module.canCreateRecord && !hideClone && isCreated && !settings.hideClone"
v-if="!inEditing && isCreated && module.canCreateRecord && !(hideClone || settings.hideClone)"
data-test-id="button-clone"
variant="light"
size="lg"
@@ -117,7 +117,7 @@
</b-button>
<b-button
v-if="!inEditing && !settings.hideEdit && isCreated"
v-if="!inEditing && isCreated && !(hideEdit || settings.hideEdit)"
data-test-id="button-edit"
:disabled="!record.canUpdateRecord || processing"
variant="light"
@@ -129,7 +129,7 @@
</b-button>
<b-button
v-if="module.canCreateRecord && !hideAdd && !inEditing && !settings.hideNew"
v-if="!inEditing && module.canCreateRecord && !(hideNew || settings.hideNew)"
data-test-id="button-add-new"
variant="primary"
size="lg"
@@ -141,7 +141,7 @@
</b-button>
<b-button
v-if="inEditing && !settings.hideSubmit"
v-if="inEditing && !(hideSubmit || settings.hideSubmit)"
data-test-id="button-submit"
:disabled="!canSaveRecord || processing"
class="d-flex align-items-center justify-content-center ml-2"
@@ -162,6 +162,8 @@
{{ labels.submit || $t('label.save') }}
</span>
</b-button>
<slot name="actions" />
</template>
</b-col>
</b-row>
@@ -220,14 +222,34 @@ export default {
required: true,
},
hideClone: {
hideBack: {
type: Boolean,
default: () => false,
default: () => true,
},
hideAdd: {
hideDelete: {
type: Boolean,
default: () => false,
default: () => true,
},
hideNew: {
type: Boolean,
default: () => true,
},
hideClone: {
type: Boolean,
default: () => true,
},
hideEdit: {
type: Boolean,
default: () => true,
},
hideSubmit: {
type: Boolean,
default: () => true,
},
showRecordModal: {

View File

@@ -16,9 +16,9 @@
ref="layoutSelect"
size="sm"
:value="layout.pageLayoutID"
:options="layouts"
value-field="pageLayoutID"
text-field="label"
:options="layouts"
style="width: 300px;"
@change="switchLayout"
/>

View File

@@ -212,6 +212,7 @@
<draggable
v-model="layouts"
handle=".handle"
group="layouts"
tag="b-tbody"
>
<tr
@@ -231,7 +232,6 @@
<b-form-input
v-model="layout.meta.title"
:state="layoutTitleState(layout.meta.title)"
@input="layout.meta.updated = true"
/>
</b-td>
@@ -242,7 +242,6 @@
<b-form-input
v-model="layout.handle"
:state="layoutHandleState(layout.handle)"
@input="layout.meta.updated = true"
/>
<b-input-group-append>
@@ -274,9 +273,8 @@
class="text-center align-middle"
>
<c-input-confirm
:title="$t('tabs.tooltip.delete')"
class="ml-2"
@confirmed="deleteLayout(index)"
@confirmed="removeLayout(index)"
/>
</td>
</tr>
@@ -306,6 +304,10 @@
@cancel="layoutEditor.layout = undefined"
@hide="layoutEditor.layout = undefined"
>
<h5 class="mb-3">
Visibility
</h5>
<b-form-group
label="Condition"
label-class="text-primary"
@@ -340,6 +342,157 @@
class="bg-white"
/>
</b-form-group>
<template v-if="isRecordPage">
<hr>
<h5 class="mb-3">
Record toolbar
</h5>
<b-form-group
label="Buttons"
label-class="text-primary"
>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.back.enabled"
>
Show back button
</b-form-checkbox>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.delete.enabled"
>
Show delete button
</b-form-checkbox>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.clone.enabled"
>
Show clone button
</b-form-checkbox>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.new.enabled"
>
Show new button
</b-form-checkbox>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.edit.enabled"
>
Show edit button
</b-form-checkbox>
<b-form-checkbox
v-model="layoutEditor.layout.config.buttons.submit.enabled"
>
Show submit button
</b-form-checkbox>
</b-form-group>
<b-form-group
label="Actions"
label-class="text-primary"
>
<b-table-simple
responsive="lg"
borderless
small
>
<b-thead>
<tr>
<th />
<th
class="text-primary"
style="width: 25%; min-width: 190px;"
>
Label
</th>
<th
class="text-primary"
style="width: 33%; min-width: 240px;"
>
Type
</th>
<th
class="text-primary"
style="width: 33%; min-width: 240px;"
>
Options
</th>
<th style="min-width: 80px;" />
</tr>
</b-thead>
<draggable
v-model="layoutEditor.layout.config.actions"
handle=".handle"
group="actions"
tag="b-tbody"
>
<tr
v-for="(action, index) in layoutEditor.layout.config.actions"
:key="index"
>
<b-td class="handle text-center align-middle pr-2">
<font-awesome-icon
:icon="['fas', 'bars']"
class="grab m-0 text-light p-0"
/>
</b-td>
<b-td
class="align-middle"
>
<b-form-input
v-model="action.meta.label"
/>
</b-td>
<b-td
class="align-middle"
>
<b-form-select
v-model="action.kind"
:options="actionKindOptions"
/>
</b-td>
<b-td
class="align-middle"
>
<b-form-select
v-model="action.params.pageLayoutID"
:options="actionLayoutOptions"
value-field="pageLayoutID"
text-field="label"
/>
</b-td>
<td
class="text-center align-middle"
>
<c-input-confirm
class="ml-2"
@confirmed="removeLayoutAction(index)"
/>
</td>
</tr>
</draggable>
</b-table-simple>
<b-button
variant="primary"
@click="addLayoutAction"
>
Add action
</b-button>
</b-form-group>
</template>
</b-modal>
<b-modal
@@ -536,7 +689,7 @@ export default {
layout: undefined,
},
deletedLayouts: new Set(),
removedLayouts: new Set(),
roles: {
processing: false,
@@ -627,6 +780,20 @@ export default {
this.$set(this.layoutEditor.layout.config.visibility, 'roles', roles)
},
},
actionKindOptions () {
return [
{ value: 'toLayout', text: 'Go to layout' },
]
},
actionLayoutOptions () {
return [
{ pageLayoutID: '', label: 'No route selected' },
...this.layouts.filter(({ pageLayoutID }) => pageLayoutID !== NoID)
.map(({ pageLayoutID, handle, meta }) => ({ pageLayoutID, label: meta.title || handle || pageLayoutID })),
]
},
},
watch: {
@@ -636,7 +803,7 @@ export default {
this.page = undefined
this.layouts = []
this.deletedLayouts = new Set()
this.removedLayouts = new Set()
if (pageID) {
this.processing = true
@@ -674,7 +841,7 @@ export default {
async fetchLayouts () {
const { namespaceID } = this.namespace
return this.findLayoutsByPageID({ namespaceID, pageID: this.pageID }).then(layouts => {
this.layouts = layouts.map(layout => new compose.PageLayout(layout))
this.layouts = layouts
})
},
@@ -699,10 +866,10 @@ export default {
this.layoutEditor.layout = undefined
},
deleteLayout (index) {
removeLayout (index) {
const { pageLayoutID } = this.layouts[index] || {}
if (pageLayoutID !== NoID) {
this.deletedLayouts.add(this.layouts[index])
this.removedLayouts.add(this.layouts[index])
}
this.layouts.splice(index, 1)
@@ -710,12 +877,12 @@ export default {
configureLayout (index) {
this.layoutEditor.index = index
this.layoutEditor.layout = { ...this.layouts[index] }
this.layoutEditor.layout = new compose.PageLayout(this.layouts[index])
},
async handleSaveLayouts () {
// Delete first so old deleted handles don't interfere with new identical ones
return Promise.all([...this.deletedLayouts].map(this.deletePageLayout)).then(() => {
return Promise.all([...this.removedLayouts].map(this.deletePageLayout)).then(() => {
return Promise.all(this.layouts.map(layout => {
if (layout.pageLayoutID === NoID) {
return this.createPageLayout(layout)
@@ -754,7 +921,7 @@ export default {
}).then(this.handlePageLayoutReorder)
.then(() => {
this.fetchLayouts()
this.deletedLayouts = new Set()
this.removedLayouts = new Set()
this.toastSuccess(this.$t('notification:page.saved'))
if (closeOnSuccess) {
@@ -791,6 +958,14 @@ export default {
.catch(this.toastErrorHandler(this.$t('notification:page.iconFetchFailed')))
},
addLayoutAction () {
this.layoutEditor.layout.addAction()
},
removeLayoutAction (index) {
this.layoutEditor.layout.config.actions.splice(index, 1)
},
async saveIcon () {
return this.$ComposeAPI.pageUpdateIcon({
namespaceID: this.namespace.namespaceID,

View File

@@ -40,6 +40,12 @@
:in-editing="inEditing"
:show-record-modal="showRecordModal"
:record-navigation="recordNavigation"
:hide-back="!layoutButtons.has('back')"
:hide-delete="!layoutButtons.has('delete')"
:hide-new="!layoutButtons.has('new')"
:hide-clone="!layoutButtons.has('clone')"
:hide-edit="!layoutButtons.has('edit')"
:hide-submit="!layoutButtons.has('submit')"
@add="handleAdd()"
@clone="handleClone()"
@edit="handleEdit()"
@@ -48,7 +54,20 @@
@back="handleBack()"
@submit="handleFormSubmit('page.record')"
@update-navigation="handleRedirectToPrevOrNext"
/>
>
<template #actions>
<b-button
v-for="(action, index) in layoutActions"
:key="index"
variant="primary"
size="lg"
class="ml-2"
@click.prevent="determineLayout(action.params.pageLayoutID)"
>
{{ action.Meta.label }}
</b-button>
</template>
</record-toolbar>
</portal>
</div>
</template>
@@ -114,6 +133,7 @@ export default {
layouts: [],
layout: undefined,
layoutButtons: {},
blocks: [],
}
@@ -146,13 +166,22 @@ export default {
recordToolbarLabels () {
// Use an intermediate object so we can reflect all changes in one go;
const aux = {}
const { buttons = {} } = this.page.config || {}
const { config = {} } = this.layout
const { buttons = {} } = config
Object.entries(buttons).forEach(([key, { label = '' }]) => {
aux[key] = label
})
return aux
},
layoutActions () {
const { config = {} } = this.layout
const { actions = [] } = config
return actions
},
title () {
const { name, handle } = this.module
const titlePrefix = this.inCreating ? 'create' : this.inEditing ? 'edit' : 'view'
@@ -178,6 +207,7 @@ export default {
'page.pageID': {
immediate: true,
handler () {
this.layouts = this.getPageLayouts(this.page.pageID)
this.determineLayout()
},
},
@@ -287,16 +317,27 @@ export default {
}
},
determineLayout () {
this.layouts = this.getPageLayouts(this.page.pageID)
determineLayout (pageLayoutID) {
this.layout = this.layouts.find(l => {
const { roles = [] } = l.config.visibility
if (pageLayoutID && l.pageLayoutID !== pageLayoutID) return
if (!roles.length) return true
return this.$auth.user.roles.some(roleID => roles.includes(roleID))
})
const { config = {} } = this.layout
const { buttons = [] } = config
this.layoutButtons = Object.entries(buttons).reduce((acc, [key, value]) => {
if (value.enabled) {
acc.add(key)
}
return acc
}, new Set())
this.blocks = (this.layout || {}).blocks.map(({ blockID, xywh }) => {
const block = this.page.blocks.find(b => b.blockID === blockID)
block.xywh = xywh

View File

@@ -8,22 +8,25 @@ export type PageLayoutInput = PageLayout | Partial<PageLayout>
interface PageLayoutConfig {
visibility: Visibility;
buttons: {
submit: Button;
back: Button;
delete: Button;
new: Button;
edit: Button;
clone: Button;
back: Button;
edit: Button;
submit: Button;
};
actions: Action[];
}
interface Action {
actionID: string;
kind: string;
placement: string;
params: unknown;
meta: unknown;
meta: ActionMeta;
}
interface ActionMeta {
label: string;
}
interface Visibility {
@@ -52,12 +55,12 @@ export class PageLayout {
roles: [],
},
buttons: {
submit: { enabled: true },
back: { enabled: true },
delete: { enabled: true },
clone: { enabled: true },
new: { enabled: true },
edit: { enabled: true },
clone: { enabled: true },
back: { enabled: true },
submit: { enabled: true },
},
actions: [],
}
@@ -100,6 +103,19 @@ export class PageLayout {
return new PageLayout(JSON.parse(JSON.stringify(this)))
}
addAction () {
this.config.actions.push({
kind: 'toLayout',
placement: '',
params: {
pageLayoutID: '',
},
meta: {
label: '',
},
} as Action)
}
/**
* Returns resource ID
*/