diff --git a/client/web/compose/src/components/Admin/Module/RelatedPages.vue b/client/web/compose/src/components/Admin/Module/RelatedPages.vue index 04a8a6fa2..5870c4dd5 100644 --- a/client/web/compose/src/components/Admin/Module/RelatedPages.vue +++ b/client/web/compose/src/components/Admin/Module/RelatedPages.vue @@ -133,8 +133,8 @@ export default { blocks, }) - this.createPage(page).then(({ pageID, blocks }) => { - const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title: 'Primary' } }) + this.createPage(page).then(({ pageID, title, blocks }) => { + const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title } }) return this.createPageLayout(pageLayout) }).catch(this.toastErrorHandler(this.$t('notification:module.recordPage.createFailed'))) .finally(() => { @@ -166,8 +166,8 @@ export default { }) this.createPage(page) - .then(({ pageID = NoID, blocks }) => { - const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title: 'Primary' } }) + .then(({ pageID, title, blocks }) => { + const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title } }) return Promise.all([ this.updatePage({ ...this.recordPage, selfID: pageID }), this.createPageLayout(pageLayout), diff --git a/client/web/compose/src/components/Common/RecordToolbar.vue b/client/web/compose/src/components/Common/RecordToolbar.vue index fc30db2bd..43bc6edbb 100644 --- a/client/web/compose/src/components/Common/RecordToolbar.vue +++ b/client/web/compose/src/components/Common/RecordToolbar.vue @@ -113,7 +113,7 @@ class="ml-2" @click.prevent="$emit('clone')" > - {{ labels.clone || $t('label.clone') }} + {{ labels.clone || $t('label.saveAsCopy') }} { + if (!this.$refs.map) return this.$refs.map.mapObject.invalidateSize() }) }) diff --git a/client/web/compose/src/components/PageBlocks/TabsConfigurator.vue b/client/web/compose/src/components/PageBlocks/TabsConfigurator.vue index a48e9d734..157a22017 100644 --- a/client/web/compose/src/components/PageBlocks/TabsConfigurator.vue +++ b/client/web/compose/src/components/PageBlocks/TabsConfigurator.vue @@ -300,7 +300,7 @@ export default { computed: { blockOptions () { return [ - ...this.page.blocks.filter(({ blockID, kind }) => kind !== 'Tabs' && !this.blocks.some(b => b.blockID === blockID)), + ...this.page.blocks.filter(({ blockID, kind }) => kind !== 'Tabs' && !this.blocks.some(b => b.blockID === blockID) && this.options.tabs.some(b => b.blockID === blockID)), ...this.blocks.filter(b => b.kind !== 'Tabs'), ].map(b => ({ ...b, value: fetchID(b) })) }, diff --git a/client/web/compose/src/views/Admin/Pages/Builder.vue b/client/web/compose/src/views/Admin/Pages/Builder.vue index 43c01057a..273f175d2 100644 --- a/client/web/compose/src/views/Admin/Pages/Builder.vue +++ b/client/web/compose/src/views/Admin/Pages/Builder.vue @@ -247,13 +247,11 @@ !this.usedBlocks.some(b => b.blockID === blockID)) }, @@ -499,6 +489,7 @@ export default { loadPages: 'page/load', findLayoutByID: 'pageLayout/findByID', findLayoutsByPageID: 'pageLayout/findByPageID', + createPageLayout: 'pageLayout/create', updatePageLayout: 'pageLayout/update', deletePageLayout: 'pageLayout/delete', }), @@ -560,35 +551,41 @@ export default { }) } - // Changes meta.hidden property to false, for all blocks that were tabbed only in the deleted block - if (this.blocks[index].kind === 'Tabs') { - const tabbedBlocks = this.blocks.filter((block) => block.kind === 'Tabs' && fetchID(block) !== fetchID(this.blocks[index])) - .map(({ options }) => options.tabs).flat().reduce((unique, o) => { - if (!unique.some(tab => tab.blockID === o.blockID)) { - unique.push(o) - } - return unique - }, []).map(({ blockID }) => blockID) - - this.blocks[index].options.tabs.forEach(({ blockID }) => { - if (tabbedBlocks.includes(blockID)) return - const index = this.blocks.findIndex((b) => fetchID(b) === blockID) - if (index === -1) return - this.blocks[index].meta.hidden = false - this.calculateNewBlockPosition(this.blocks[index]) - }) - } + const { kind } = this.blocks[index] this.blocks.splice(index, 1) + this.unsavedBlocks.add(index) + + if (kind === 'Tabs') { + this.showUntabbedHiddenBlocks() + } if (this.editor) this.editor = undefined - this.unsavedBlocks.add(index) + }, + + // Changes meta.hidden property to false, for all blocks that are hidden but not in a tab + showUntabbedHiddenBlocks () { + const tabbedBlocks = new Set() + + this.blocks.forEach(block => { + if (block.kind !== 'Tabs') return + + block.options.tabs.forEach(({ blockID }) => tabbedBlocks.add(blockID)) + }) + + this.blocks.forEach((block, index) => { + if (!block.meta.hidden || tabbedBlocks.has(fetchID(block))) return + + this.blocks[index].meta.hidden = false + this.calculateNewBlockPosition(this.blocks[index]) + }) }, onBlockUpdated (index) { this.unsavedBlocks.add(index) }, + // When debugging this, make sure to remove the @hide event handle from the block editor/creator modals updateBlocks (block = this.editor.block) { block = compose.PageBlockMaker(block) @@ -677,6 +674,30 @@ export default { }) }, + validateModuleFieldSelection (module, page) { + // Find all required fields + const req = new Set(module.fields.filter(({ isRequired = false }) => isRequired).map(({ name }) => name)) + + // Check if all required fields are there + for (const b of page.blocks) { + if (b.kind !== 'Record') { + continue + } + + // If no fields are in Record block, means all fields are present(default), no need to check + if (!b.options || !b.options.fields.length) { + return true + } + + for (const f of b.options.fields) { + req.delete(f.name) + } + } + + // If required fields are satisfied, then the validation passes + return !req.size + }, + async handleSaveLayout ({ closeOnSuccess = false, previewOnSuccess = false } = {}) { const { namespaceID } = this.namespace @@ -750,34 +771,33 @@ export default { }).catch(this.toastErrorHandler(this.$t('notification:page.page-layout.save.failed'))) }, - validateModuleFieldSelection (module, page) { - // Find all required fields - const req = new Set(module.fields.filter(({ isRequired = false }) => isRequired).map(({ name }) => name)) + handleCloneLayout () { + this.processing = true - // Check if all required fields are there - for (const b of page.blocks) { - if (b.kind !== 'Record') { - continue - } - - // If no fields are in Record block, means all fields are present(default), no need to check - if (!b.options || !b.options.fields.length) { - return true - } - - for (const f of b.options.fields) { - req.delete(f.name) - } + const layout = { + ...this.layout, + handle: '', + weight: this.layouts.length + 1, } - // If required fields are satisfied, then the validation passes - return !req.size + layout.meta.title = `${this.$t('copyOf')}${layout.meta.title}` + + this.createPageLayout(this.layout).then(({ layoutID }) => { + return this.fetchPageLayouts().then(() => { + this.switchLayout(layoutID) + this.toastSuccess(this.$t('notification:page.page-layout.clone.success')) + }) + }).finally(() => { + this.processing = false + }).catch(this.toastErrorHandler(this.$t('notification:page.page-layout.clone.failed'))) }, handleDeleteLayout () { this.processing = true this.deletePageLayout({ ...this.layout }).then(() => { + return this.fetchPageLayouts() + }).then(() => { this.setLayout() this.toastSuccess(this.$t('notification:page.page-layout.delete.success')) }).finally(() => { @@ -868,9 +888,10 @@ export default { const tempBlocks = [] const { blocks = [] } = this.layout || {} - blocks.forEach(({ blockID, xywh }) => { + blocks.forEach(({ blockID, xywh, meta }) => { let block = this.page.blocks.find(b => b.blockID === blockID) block.xywh = xywh + block.meta.hidden = meta.hidden tempBlocks.push(block) if (block.kind === 'Tabs') { diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue index c1ff83529..4f98e704f 100644 --- a/client/web/compose/src/views/Admin/Pages/Edit.vue +++ b/client/web/compose/src/views/Admin/Pages/Edit.vue @@ -304,6 +304,7 @@ size="lg" @ok="updateLayout()" @cancel="layoutEditor.layout = undefined" + @hide="layoutEditor.layout = undefined" > @@ -443,7 +445,7 @@ :disable-save="disableSave" :processing="processing" @clone="handleClone()" - @delete="handleDeletePage" + @delete="handleDeletePage()" @save="handleSave()" @saveAndClose="handleSave({ closeOnSuccess: true })" > diff --git a/client/web/compose/src/views/Admin/Pages/List.vue b/client/web/compose/src/views/Admin/Pages/List.vue index 9cf5449ea..cc16b23cf 100644 --- a/client/web/compose/src/views/Admin/Pages/List.vue +++ b/client/web/compose/src/views/Admin/Pages/List.vue @@ -148,8 +148,8 @@ export default { handleAddPageFormSubmit () { const { namespaceID } = this.namespace this.page.weight = this.tree.length - this.createPage({ ...this.page, namespaceID }).then(({ pageID }) => { - const pageLayout = new compose.PageLayout({ namespaceID, pageID, meta: { title: 'Primary' } }) + this.createPage({ ...this.page, namespaceID }).then(({ pageID, title }) => { + const pageLayout = new compose.PageLayout({ namespaceID, pageID, meta: { title } }) return this.createPageLayout(pageLayout).then(() => { this.$router.push({ name: 'admin.pages.edit', params: { pageID } }) }) diff --git a/client/web/compose/src/views/Public/Pages/Records/View.vue b/client/web/compose/src/views/Public/Pages/Records/View.vue index 33fff050a..5f98c855c 100644 --- a/client/web/compose/src/views/Public/Pages/Records/View.vue +++ b/client/web/compose/src/views/Public/Pages/Records/View.vue @@ -21,6 +21,7 @@ v-bind="$props" :errors="errors" :record="record" + :blocks="blocks" :mode="inEditing ? 'editor' : 'base'" @reload="loadRecord()" /> @@ -51,6 +52,7 @@ + diff --git a/client/web/compose/src/views/Public/Pages/View.vue b/client/web/compose/src/views/Public/Pages/View.vue index 629d23770..42730d58a 100644 --- a/client/web/compose/src/views/Public/Pages/View.vue +++ b/client/web/compose/src/views/Public/Pages/View.vue @@ -177,32 +177,15 @@ export default { 'page.pageID': { immediate: true, - handler (pageID) { + handler () { + this.determineLayout() + // If the page changed we need to clear the record pagination since its not relevant anymore if (this.recordPaginationUsable) { this.setRecordPaginationUsable(false) } else { this.clearRecordIDs() } - - this.layouts = this.getPageLayouts(pageID) - this.layout = this.layouts.find(l => { - const { roles = [] } = l.config.visibility - - if (!roles.length) return true - - return this.$auth.user.roles.some(roleID => roles.includes(roleID)) - }) - - const { meta = {} } = this.layout || {} - const title = meta.title || this.page.title - document.title = [title, this.namespace.name, this.$t('general:label.app-name.public')].filter(v => v).join(' | ') - - this.blocks = (this.layout || {}).blocks.map(({ blockID, xywh }) => { - const block = this.page.blocks.find(b => b.blockID === blockID) - block.xywh = xywh - return block - }) }, }, }, @@ -224,6 +207,27 @@ export default { setRecordPaginationUsable: 'ui/setRecordPaginationUsable', clearRecordIDs: 'ui/clearRecordIDs', }), + + determineLayout () { + this.layouts = this.getPageLayouts(this.page.pageID) + this.layout = this.layouts.find(l => { + const { roles = [] } = l.config.visibility + + if (!roles.length) return true + + return this.$auth.user.roles.some(roleID => roles.includes(roleID)) + }) + + const { meta = {} } = this.layout || {} + const title = meta.title || this.page.title + document.title = [title, this.namespace.name, this.$t('general:label.app-name.public')].filter(v => v).join(' | ') + + this.blocks = (this.layout || {}).blocks.map(({ blockID, xywh }) => { + const block = this.page.blocks.find(b => b.blockID === blockID) + block.xywh = xywh + return block + }) + }, }, } diff --git a/lib/js/src/compose/types/page-layout.ts b/lib/js/src/compose/types/page-layout.ts index 492905a21..5f11f46ea 100644 --- a/lib/js/src/compose/types/page-layout.ts +++ b/lib/js/src/compose/types/page-layout.ts @@ -7,6 +7,14 @@ export type PageLayoutInput = PageLayout | Partial interface PageLayoutConfig { visibility: Visibility; + buttons: { + submit: Button; + delete: Button; + new: Button; + edit: Button; + clone: Button; + back: Button; + }; actions: Action[]; } @@ -43,6 +51,14 @@ export class PageLayout { expression: '', roles: [], }, + buttons: { + submit: { enabled: true }, + delete: { enabled: true }, + new: { enabled: true }, + edit: { enabled: true }, + clone: { enabled: true }, + back: { enabled: true }, + }, actions: [], } diff --git a/locale/en/corteza-webapp-compose/notification.yaml b/locale/en/corteza-webapp-compose/notification.yaml index af4cd2568..8b55671d1 100644 --- a/locale/en/corteza-webapp-compose/notification.yaml +++ b/locale/en/corteza-webapp-compose/notification.yaml @@ -102,6 +102,9 @@ page: save: success: Layout saved failed: Failed to save layout + clone: + success: Layout created + failed: Failed to clone layout delete: success: Layout deleted failed: Failed to delete layout