Add page layouts for record pages

This commit is contained in:
Jože Fortun
2023-04-07 12:39:27 +02:00
parent 4f31add6a4
commit b14b9c5c22
11 changed files with 165 additions and 86 deletions
@@ -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),
@@ -113,7 +113,7 @@
class="ml-2"
@click.prevent="$emit('clone')"
>
{{ labels.clone || $t('label.clone') }}
{{ labels.clone || $t('label.saveAsCopy') }}
</b-button>
<b-button
@@ -69,6 +69,7 @@ import { LPolygon, LControl } from 'vue2-leaflet'
import { compose, NoID } from '@cortezaproject/corteza-js'
import { mapGetters, mapActions } from 'vuex'
import { evaluatePrefilter } from 'corteza-webapp-compose/src/lib/record-filter'
import { throttle } from 'lodash'
import base from './base'
export default {
@@ -133,11 +134,9 @@ export default {
},
},
boundingRect: {
handler () {
this.loadEvents()
},
},
boundingRect: throttle(function () {
this.loadEvents()
}, 300),
},
created () {
@@ -217,6 +216,7 @@ export default {
this.processing = false
setTimeout(() => {
if (!this.$refs.map) return
this.$refs.map.mapObject.invalidateSize()
})
})
@@ -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) }))
},
@@ -247,13 +247,11 @@
<editor-toolbar
:back-link="{name: 'admin.pages'}"
:hide-save="!page.canUpdatePage"
:disable-clone="disableClone"
:disable-save="processing"
:clone-tooltip="cloneTooltip"
:processing="processing"
@save="handleSaveLayout()"
@delete="handleDeleteLayout"
@delete="handleDeleteLayout()"
@saveAndClose="handleSaveLayout({ closeOnSuccess: true })"
@clone="handleClone()"
@clone="handleCloneLayout()"
>
<b-button
v-if="page.canUpdatePage"
@@ -406,14 +404,6 @@ export default {
return this.hasChildren || !this.page.canDeletePage || !!this.page.deletedAt
},
disableClone () {
return !!this.module
},
cloneTooltip () {
return this.disableClone ? this.$t('tooltip.saveAsCopy') : ''
},
selectableExistingBlocks () {
return this.page.blocks.filter(({ blockID }) => !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') {
@@ -304,6 +304,7 @@
size="lg"
@ok="updateLayout()"
@cancel="layoutEditor.layout = undefined"
@hide="layoutEditor.layout = undefined"
>
<b-form-group
label="Condition"
@@ -317,6 +318,7 @@
</b-input-group-prepend>
<b-form-input
v-model="layoutEditor.layout.config.visibility.expression"
placeholder="When will the layout be shown"
/>
</b-input-group>
</b-form-group>
@@ -443,7 +445,7 @@
:disable-save="disableSave"
:processing="processing"
@clone="handleClone()"
@delete="handleDeletePage"
@delete="handleDeletePage()"
@save="handleSave()"
@saveAndClose="handleSave({ closeOnSuccess: true })"
>
@@ -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 } })
})
@@ -21,6 +21,7 @@
v-bind="$props"
:errors="errors"
:record="record"
:blocks="blocks"
:mode="inEditing ? 'editor' : 'base'"
@reload="loadRecord()"
/>
@@ -51,6 +52,7 @@
</portal>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Grid from 'corteza-webapp-compose/src/components/Public/Page/Grid'
@@ -97,6 +99,7 @@ export default {
required: false,
default: '',
},
// Open record in a modal
showRecordModal: {
type: Boolean,
@@ -108,12 +111,18 @@ export default {
return {
inEditing: false,
inCreating: false,
layouts: [],
layout: undefined,
blocks: [],
}
},
computed: {
...mapGetters({
getNextAndPrevRecord: 'ui/getNextAndPrevRecord',
getPageLayouts: 'pageLayout/getByPageID',
}),
portalTopbarTitle () {
@@ -165,6 +174,13 @@ export default {
this.loadRecord()
},
},
'page.pageID': {
immediate: true,
handler () {
this.determineLayout()
},
},
},
created () {
@@ -270,6 +286,23 @@ export default {
})
}
},
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))
})
this.blocks = (this.layout || {}).blocks.map(({ blockID, xywh }) => {
const block = this.page.blocks.find(b => b.blockID === blockID)
block.xywh = xywh
return block
})
},
},
}
</script>
@@ -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
})
},
},
}
</script>
+16
View File
@@ -7,6 +7,14 @@ export type PageLayoutInput = PageLayout | Partial<PageLayout>
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: [],
}
@@ -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