Add page layouts for record pages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user