Update multi page layout interaction with tabs block

This commit is contained in:
Jože Fortun
2023-04-07 12:39:27 +02:00
parent 0b98c786dd
commit a03025fb5f
13 changed files with 155 additions and 103 deletions
@@ -46,6 +46,7 @@
<vue-select
v-model="selectedExistingBlock"
:get-option-label="getBlockLabel"
:get-option-key="b => b.blockID"
:options="existingBlocks"
:calculate-position="calculateDropdownPosition"
placeholder="Blocks from other layouts"
@@ -109,7 +109,7 @@ export default {
},
drillDownOptions () {
return this.page.blocks.filter(({ blockID, kind, options = {} }) => kind === 'RecordList' && blockID !== NoID && options.moduleID === this.selectedChartModuleID)
return this.blocks.filter(({ blockID, kind, options = {} }) => kind === 'RecordList' && blockID !== NoID && options.moduleID === this.selectedChartModuleID)
},
},
@@ -836,8 +836,8 @@ export default {
disableInlineEditor () {
const thisModuleID = this.options.moduleID
// Finds another inline editor block with the same recordListModulea as this one
const otherInlineWithSameModule = !!this.page.blocks.find(({ kind, options }, index) => {
// Finds another inline editor block with the same recordListModule as this one
const otherInlineWithSameModule = this.blocks.some(({ kind, options }, index) => {
if (this.blockIndex !== index) {
return kind === 'RecordList' && options.editable && options.moduleID === thisModuleID
}
@@ -82,16 +82,16 @@ export default {
computed: {
tabbedBlocks () {
return this.block.options.tabs.map(({ blockID, title }) => {
let block = this.page.blocks.find(b => fetchID(b) === blockID)
block = block ? compose.PageBlockMaker(block) : undefined
let block = this.blocks.find(b => fetchID(b) === blockID)
// Blocks should display as Plain, to avoid card shadow/border
if (block) {
block.style.wrap.kind = 'Plain'
block = compose.PageBlockMaker(block)
}
return {
block: block ? compose.PageBlockMaker(block) : undefined,
block,
title,
}
})
@@ -168,8 +168,7 @@
id="popover-edit"
size="sm"
variant="light"
:title="blockEditDisabled ? $t('tabs.tooltip.editDisabled') : $t('tabs.tooltip.edit')"
:disabled="blockEditDisabled"
:title="$t('tabs.tooltip.edit')"
class="d-flex align-items-center justify-content-center"
style="width: 40px;"
@click="editBlock(tab.blockID)"
@@ -300,11 +299,10 @@ export default {
computed: {
blockOptions () {
return this.page.blocks.filter(b => b.kind !== 'Tabs').map(b => ({ ...b, value: fetchID(b) }))
},
blockEditDisabled () {
return this.page.blocks.find(b => fetchID(b) === fetchID(this.block)) === undefined
return [
...this.page.blocks.filter(({ blockID, kind }) => kind !== 'Tabs' && !this.blocks.some(b => b.blockID === blockID)),
...this.blocks.filter(b => b.kind !== 'Tabs'),
].map(b => ({ ...b, value: fetchID(b) }))
},
},
@@ -33,6 +33,11 @@ export default {
required: true,
},
blocks: {
type: Array,
default: () => [],
},
block: {
type: compose.PageBlock,
required: true,
@@ -9,7 +9,12 @@
slot-scope="{ boundingRect, block, index }"
>
<page-block
v-bind="{ ...$attrs, block, page, boundingRect, blockIndex: index }"
v-bind="{ ...$attrs }"
:page="page"
:blocks="page.blocks"
:block="block"
:bounding-rect="boundingRect"
:block-index="index"
class="p-2"
v-on="$listeners"
/>
+5 -6
View File
@@ -67,17 +67,16 @@ export default function (ComposeAPI) {
})
},
async findByID ({ commit, getters }, { namespaceID, pageLayoutID, force = false } = {}) {
async findByID ({ commit, getters }, { namespaceID, pageID, pageLayoutID, force = false } = {}) {
if (!force) {
const oldItem = getters.getByID(pageLayoutID)
if (oldItem) {
return new Promise((resolve) => resolve(oldItem))
}
return new Promise((resolve) => resolve(oldItem))
}
commit(types.pending)
return ComposeAPI.pageLayoutRead({ namespaceID, pageLayoutID }).then(pl => {
return ComposeAPI.pageLayoutRead({ namespaceID, pageID, pageLayoutID }).then(pl => {
const pageLayout = new compose.PageLayout(pl)
commit(types.updateSet, [pageLayout])
return pageLayout
}).finally(() => {
@@ -122,7 +121,7 @@ export default function (ComposeAPI) {
})
},
async delete ({ commit, dispatch }, item) {
async delete ({ commit }, item) {
commit(types.pending)
return ComposeAPI.pageLayoutDelete(item).then(() => {
commit(types.removeFromSet, [item])
@@ -136,10 +136,11 @@
...$props
}"
:page="page"
:module="module"
:record="record"
:blocks="usedBlocks"
:block-index="index"
:block="block"
:module="module"
:record="record"
:bounding-rect="boundingRect"
editable
class="p-2"
@@ -183,6 +184,7 @@
:namespace="namespace"
:module="module"
:page="page"
:blocks="usedBlocks"
:block.sync="editor.block"
:record="record"
/>
@@ -202,10 +204,12 @@
:namespace="namespace"
:module="module"
:page="page"
:blocks="usedBlocks"
:block.sync="editor.block"
:block-index="editor.index"
:record="record"
/>
<template #modal-footer="{ cancel }">
<c-input-confirm
size="md"
@@ -242,14 +246,13 @@
<portal to="admin-toolbar">
<editor-toolbar
:back-link="{name: 'admin.pages'}"
:hide-delete="hideDelete"
:hide-save="!page.canUpdatePage"
:disable-clone="disableClone"
:disable-save="processing"
:clone-tooltip="cloneTooltip"
@save="handleSave()"
@delete="handleDeletePage"
@saveAndClose="handleSave({ closeOnSuccess: true })"
@save="handleSaveLayout()"
@delete="handleDeleteLayout"
@saveAndClose="handleSaveLayout({ closeOnSuccess: true })"
@clone="handleClone()"
>
<b-button
@@ -262,29 +265,6 @@
>
+ {{ $t('build.addBlock') }}
</b-button>
<template #delete>
<b-dropdown
v-if="showDeleteDropdown"
data-test-id="dropdown-delete"
size="lg"
variant="danger"
:text="$t('general:label.delete')"
>
<b-dropdown-item
data-test-id="dropdown-item-delete-update-parent-of-sub-pages"
@click="handleDeletePage('rebase')"
>
{{ $t('delete.rebase') }}
</b-dropdown-item>
<b-dropdown-item
data-test-id="dropdown-item-delete-sub-pages"
@click="handleDeletePage('cascade')"
>
{{ $t('delete.cascade') }}
</b-dropdown-item>
</b-dropdown>
</template>
</editor-toolbar>
</portal>
@@ -426,10 +406,6 @@ export default {
return this.hasChildren || !this.page.canDeletePage || !!this.page.deletedAt
},
showDeleteDropdown () {
return this.hasChildren && this.page.canDeletePage && !this.page.deletedAt
},
disableClone () {
return !!this.module
},
@@ -439,7 +415,30 @@ export default {
},
selectableExistingBlocks () {
return this.page.blocks.filter(({ blockID }) => !this.blocks.some(b => b.blockID === blockID))
return this.page.blocks.filter(({ blockID }) => !this.usedBlocks.some(b => b.blockID === blockID))
},
// Blocks used on page or tabbed
usedBlocks () {
const tabbedIDs = new Set()
// If tab is not on layout include it
this.blocks.forEach(block => {
if (block.kind !== 'Tabs') return
const { tabs = [] } = block.options
tabs.forEach(tab => {
const { blockID } = this.page.blocks.find(({ blockID }) => blockID === tab.blockID) || {}
if (blockID) {
tabbedIDs.add(blockID)
}
})
})
return [
...this.page.blocks.filter(({ blockID }) => tabbedIDs.has(blockID)),
...this.blocks.filter(({ blockID }) => !tabbedIDs.has(blockID)),
]
},
},
@@ -501,13 +500,18 @@ export default {
findLayoutByID: 'pageLayout/findByID',
findLayoutsByPageID: 'pageLayout/findByPageID',
updatePageLayout: 'pageLayout/update',
deletePageLayout: 'pageLayout/delete',
}),
fulfilEditRequest (blockID) {
// this ensures whatever changes in tabs is not lost before we lose its configurator
// because we are reusing that modal component
this.updateBlocks()
this.blocks.find((block, i) => fetchID(block) === blockID && this.editBlock(i))
const blockIndex = this.blocks.findIndex(block => fetchID(block) === blockID)
if (blockIndex > -1) {
this.editBlock(blockIndex)
}
},
fulfilCreateRequest (block) {
@@ -542,7 +546,9 @@ export default {
},
editBlock (index = undefined) {
this.editor = { index, block: compose.PageBlockMaker(this.blocks[index]) }
this.$nextTick(() => {
this.editor = { index, block: compose.PageBlockMaker(this.blocks[index]) }
})
},
deleteBlock (index) {
@@ -586,7 +592,16 @@ export default {
updateBlocks (block = this.editor.block) {
block = compose.PageBlockMaker(block)
if (this.editor.index !== undefined) {
const creatingTabbedBlock = this.editor.block.kind !== block.kind
if (creatingTabbedBlock) {
this.$root.$emit('builder-createRequestFulfilled', {
blockID: fetchID(block),
title: block.title,
})
}
if (this.editor.index !== undefined && !creatingTabbedBlock) {
const oldBlock = this.blocks[this.editor.index]
if (oldBlock.meta.hidden === true && this.editor.block.meta.hidden === false) {
@@ -594,16 +609,8 @@ export default {
this.calculateNewBlockPosition(block)
}
if (this.editor.block.kind !== block.kind) {
this.blocks.push(block)
this.$root.$emit('builder-createRequestFulfilled', {
blockID: fetchID(block),
title: block.title,
})
} else {
this.blocks.splice(this.editor.index, 1, block)
this.unsavedBlocks.add(this.editor.index)
}
this.blocks.splice(this.editor.index, 1, block)
this.unsavedBlocks.add(this.editor.index)
} else {
this.blocks.push(block)
this.unsavedBlocks.add(this.blocks.length - 1)
@@ -612,7 +619,14 @@ export default {
if (block.kind === 'Tabs') {
block.options.tabs.forEach((tab) => {
if (!tab.blockID) return
this.blocks.find(b => fetchID(b) === tab.blockID).meta.hidden = true
let tabbedBlock = this.blocks.find(b => fetchID(b) === tab.blockID)
if (!tabbedBlock) {
tabbedBlock = this.page.blocks.find(({ blockID }) => blockID === tab.blockID)
this.blocks.push(tabbedBlock)
}
tabbedBlock.meta.hidden = true
})
}
@@ -663,7 +677,7 @@ export default {
})
},
async handleSave ({ closeOnSuccess = false, previewOnSuccess = false } = {}) {
async handleSaveLayout ({ closeOnSuccess = false, previewOnSuccess = false } = {}) {
const { namespaceID } = this.namespace
// Record blocks
@@ -674,7 +688,7 @@ export default {
// Inline record lists
const queue = []
this.blocks.forEach((b, index) => {
this.usedBlocks.forEach((b, index) => {
if (b.kind === 'RecordList' && b.options.editable) {
const p = new Promise((resolve) => {
const recordListUniqueID = [this.page.pageID, (this.record || {}).recordID || NoID, b.blockID].map(v => v || NoID).join('-')
@@ -699,12 +713,8 @@ export default {
]).then(([page, layout]) => {
const blocks = [
...page.blocks.filter(({ blockID }) => {
if (this.blocks.some(b => b.blockID === blockID)) {
return false
}
// Check if block exists in any other layout, if not delete it permanently
return this.layouts.some(({ blocks }) => blocks.some(b => b.blockID === blockID))
return !this.blocks.some(b => b.blockID === blockID) && this.layouts.some(({ pageLayoutID, blocks }) => pageLayoutID !== layout.pageLayoutID && blocks.some(b => b.blockID === blockID))
}),
...this.blocks,
]
@@ -717,7 +727,7 @@ export default {
blockID = (page.blocks.find(block => block.meta.tempID === meta.tempID) || {}).blockID
}
return { blockID, xywh }
return { blockID, xywh, meta }
})
layout = await this.updatePageLayout({ ...layout, blocks })
return { page, layout }
@@ -728,7 +738,7 @@ export default {
this.fetchPageLayouts()
this.$route.query.layoutID = layout.pageLayoutID
this.unsavedBlocks.clear()
this.toastSuccess(this.$t('notification:page.saved'))
this.toastSuccess(this.$t('notification:page.page-layout.save.success'))
if (closeOnSuccess) {
this.$router.push({ name: 'admin.pages' })
@@ -737,7 +747,7 @@ export default {
}
}).finally(() => {
this.processing = false
}).catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
}).catch(this.toastErrorHandler(this.$t('notification:page.page-layout.save.failed')))
},
validateModuleFieldSelection (module, page) {
@@ -764,10 +774,15 @@ export default {
return !req.size
},
handleDeletePage (strategy = 'abort') {
this.deletePage({ ...this.page, strategy }).then(() => {
this.$router.push({ name: 'admin.pages' })
}).catch(this.toastErrorHandler(this.$t('notification:page.deleteFailed')))
handleDeleteLayout () {
this.processing = true
this.deletePageLayout({ ...this.layout }).then(() => {
this.setLayout()
this.toastSuccess(this.$t('notification:page.page-layout.delete.success'))
}).finally(() => {
this.processing = false
}).catch(this.toastErrorHandler(this.$t('notification:page.page-layout.delete.failed')))
},
/**
@@ -835,24 +850,44 @@ export default {
async setLayout () {
if (this.$route.query.layoutID) {
const { namespaceID } = this.namespace
this.layout = await this.findLayoutByID({ namespaceID, pageLayoutID: this.$route.query.layoutID })
} else {
this.layout = this.layouts[0]
if (!this.layout) {
this.toastWarning('No layout, create one to edit it')
return this.$router.push(this.pageEditor)
}
this.layout = await this.findLayoutByID({ namespaceID, pageID: this.pageID, pageLayoutID: this.$route.query.layoutID })
}
this.layout = this.layout || this.layouts[0]
if (!this.layout) {
this.toastWarning('No layout, create one to edit it')
return this.$router.push(this.pageEditor)
}
if (this.$route.query.layoutID !== this.layout.pageLayoutID) {
this.$router.replace({ ...this.$route, query: { ...this.$route.query, layoutID: this.layout.pageLayoutID } })
}
this.unsavedBlocks.clear()
const tempBlocks = []
const { blocks = [] } = this.layout || {}
this.blocks = blocks.map(({ blockID, xywh }) => {
const block = this.page.blocks.find(b => fetchID(b) === blockID)
blocks.forEach(({ blockID, xywh }) => {
let block = this.page.blocks.find(b => b.blockID === blockID)
block.xywh = xywh
return block
tempBlocks.push(block)
if (block.kind === 'Tabs') {
const { tabs = [] } = block.options
tabs.forEach(tab => {
if (blocks.some(b => b.blockID === tab.blockID)) return
block = this.page.blocks.find(b => b.blockID === tab.blockID)
if (block) {
tempBlocks.push(block)
}
})
}
})
this.blocks = tempBlocks
},
switchLayout (layoutID) {
@@ -258,6 +258,7 @@
<b-button
variant="primary"
:disabled="layout.pageLayoutID === '0'"
class="d-flex align-items-center"
:to="{ name: 'admin.pages.builder', query: { layoutID: layout.pageLayoutID} }"
>
@@ -639,7 +640,7 @@ export default {
return this.fetchAttachments()
}).catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
this.fetchLayouts({ namespaceID, pageID }).catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
this.fetchLayouts().catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
}
},
},
@@ -662,8 +663,9 @@ export default {
deletePageLayout: 'pageLayout/delete',
}),
async fetchLayouts (payload) {
return this.findLayoutsByPageID(payload).then(layouts => {
async fetchLayouts () {
const { namespaceID } = this.namespace
return this.findLayoutsByPageID({ namespaceID, pageID: this.pageID }).then(layouts => {
this.layouts = layouts.map(layout => new compose.PageLayout(layout))
})
},
@@ -704,16 +706,16 @@ export default {
},
async handleSaveLayouts () {
return Promise.all([
...[...this.deletedLayouts].map(this.deletePageLayout),
...this.layouts.map(layout => {
// 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.layouts.map(layout => {
if (layout.pageLayoutID === NoID) {
return this.createPageLayout(layout)
} else if (layout.meta.updated) {
return this.updatePageLayout(layout)
}
}),
])
}))
})
},
handleSave ({ closeOnSuccess = false } = {}) {
@@ -727,7 +729,7 @@ export default {
this.page.config.navItem.icon = icon
return this.updatePage({ namespaceID, ...this.page, resourceTranslationLanguage }).then((page) => {
this.page = page.clone()
return this.handleSaveLayouts()
return this.handleSaveLayouts().then(this.fetchLayouts)
})
}).then(() => {
this.deletedLayouts = new Set()
@@ -149,7 +149,7 @@ export default {
const { namespaceID } = this.namespace
this.page.weight = this.tree.length
this.createPage({ ...this.page, namespaceID }).then(({ pageID }) => {
const pageLayout = new compose.PageLayout({ namespaceID, pageID })
const pageLayout = new compose.PageLayout({ namespaceID, pageID, meta: { title: 'Primary' } })
return this.createPageLayout(pageLayout).then(() => {
this.$router.push({ name: 'admin.pages.edit', params: { pageID } })
})
+1 -1
View File
@@ -68,7 +68,7 @@ export class PageLayout {
Apply(this, pl, String, 'handle')
Apply(this, pl, ISO8601Date, 'createdAt', 'updatedAt', 'deletedAt')
this.blocks = (pl.blocks || []).map(({ blockID, xywh }) => ({ blockID, xywh }))
this.blocks = (pl.blocks || []).map(({ blockID, xywh, meta }) => ({ blockID, xywh, meta }))
if (pl.meta) {
this.meta = { ...this.meta, ...pl.meta }
@@ -98,6 +98,13 @@ page:
blockWaiting: There is a page-block awaiting
copyFailed: Could not copy block {{reason}}
invalidBlock: Not a valid page-block
page-layout:
save:
success: Layout saved
failed: Failed to save layout
delete:
success: Layout deleted
failed: Failed to delete layout
record:
createFailed: Could not create record
deleteFailed: Could not delete record