diff --git a/client/web/compose/src/lib/tabs.js b/client/web/compose/src/lib/block.js
similarity index 100%
rename from client/web/compose/src/lib/tabs.js
rename to client/web/compose/src/lib/block.js
diff --git a/client/web/compose/src/mixins/pages.js b/client/web/compose/src/mixins/pages.js
index b5c1fcc4b..243af455e 100644
--- a/client/web/compose/src/mixins/pages.js
+++ b/client/web/compose/src/mixins/pages.js
@@ -1,5 +1,5 @@
import { NoID } from '@cortezaproject/corteza-js'
-import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
+import { fetchID } from 'corteza-webapp-compose/src/lib/block'
export default {
methods: {
@@ -78,6 +78,8 @@ export default {
async updateTabbedBlockIDs (page) {
// get the Tabs Block that still has tabs with tempIDs
+ let updatePage = false
+
page.blocks.filter(({ kind }) => kind === 'Tabs')
.filter(({ options = {} }) => options.tabs.some(({ blockID }) => (blockID || '').startsWith('tempID-')))
.forEach(b => {
@@ -98,9 +100,14 @@ export default {
}
b.options.tabs.splice(j, 1, tab)
+ updatePage = true
})
})
+ if (!updatePage) {
+ return page
+ }
+
return this.updatePage(page)
},
},
diff --git a/client/web/compose/src/store/index.js b/client/web/compose/src/store/index.js
index a3e9227c3..c705b7487 100644
--- a/client/web/compose/src/store/index.js
+++ b/client/web/compose/src/store/index.js
@@ -4,6 +4,7 @@ import Vuex from 'vuex'
import namespace from './namespace'
import module from './module'
import page from './page'
+import pageLayout from './page-layout'
import chart from './chart'
import user from './user'
import languages from './languages'
@@ -19,6 +20,7 @@ export default new Vuex.Store({
namespace: namespace(Vue.prototype.$ComposeAPI),
module: module(Vue.prototype.$ComposeAPI),
page: page(Vue.prototype.$ComposeAPI),
+ pageLayout: pageLayout(Vue.prototype.$ComposeAPI),
chart: chart(Vue.prototype.$ComposeAPI),
user: user(Vue.prototype.$SystemAPI),
languages: languages(Vue.prototype.$SystemAPI),
diff --git a/client/web/compose/src/store/page-layout.js b/client/web/compose/src/store/page-layout.js
new file mode 100644
index 000000000..e007882d9
--- /dev/null
+++ b/client/web/compose/src/store/page-layout.js
@@ -0,0 +1,194 @@
+import { compose } from '@cortezaproject/corteza-js'
+import * as request from '../lib/request'
+
+const types = {
+ loading: 'loading',
+ loaded: 'loaded',
+ pending: 'pending',
+ completed: 'completed',
+ updateSet: 'updateSet',
+ removeFromSet: 'removeFromSet',
+ clearSet: 'clearSet',
+}
+
+export default function (ComposeAPI) {
+ return {
+ namespaced: true,
+
+ state: {
+ loading: false,
+ pending: false,
+ set: [],
+ },
+
+ getters: {
+ loading: (state) => state.loading,
+
+ pending: (state) => state.pending,
+
+ getByID (state) {
+ return (ID) => state.set.find(({ pageLayoutID }) => ID === pageLayoutID)
+ },
+
+ getByHandle (state) {
+ return (handle) => state.set.find((pl) => handle === pl.handle)
+ },
+
+ getByPageID (state) {
+ return (ID) => state.set.filter(({ pageID }) => ID === pageID)
+ },
+
+ set (state) {
+ return state.set
+ },
+ },
+
+ actions: {
+ async load ({ commit, getters }, { namespaceID, clear = false, force = false } = {}) {
+ if (clear) {
+ commit(types.clearSet)
+ }
+
+ if (!force && getters.set.length > 1) {
+ return new Promise((resolve) => resolve(getters.set))
+ }
+
+ commit(types.loading)
+ commit(types.pending)
+ return ComposeAPI.pageLayoutListNamespace({ namespaceID }).then(({ set, filter }) => {
+ if (set && set.length > 0) {
+ commit(types.updateSet, set.map(pl => new compose.PageLayout(pl)))
+ }
+
+ return getters.set
+ }).finally(() => {
+ commit(types.loaded)
+ commit(types.completed)
+ })
+ },
+
+ async findByID ({ commit, getters }, { namespaceID, pageLayoutID, force = false } = {}) {
+ if (!force) {
+ const oldItem = getters.getByID(pageLayoutID)
+ if (oldItem) {
+ return new Promise((resolve) => resolve(oldItem))
+ }
+ }
+
+ commit(types.pending)
+ return ComposeAPI.pageLayoutRead({ namespaceID, pageLayoutID }).then(pl => {
+ const pageLayout = new compose.PageLayout(pl)
+ commit(types.updateSet, [pageLayout])
+ return pageLayout
+ }).finally(() => {
+ commit(types.completed)
+ })
+ },
+
+ async findByPageID ({ commit, getters }, { namespaceID, pageID, force = false } = {}) {
+ if (!force) {
+ const oldItems = getters.getByPageID(pageID)
+ return new Promise((resolve) => resolve(oldItems))
+ }
+
+ commit(types.pending)
+ return ComposeAPI.pageLayoutList({ namespaceID, pageID }).then(({ set }) => {
+ commit(types.updateSet, set.map(pl => new compose.PageLayout(pl)))
+ return set
+ }).finally(() => {
+ commit(types.completed)
+ })
+ },
+
+ async create ({ commit }, item) {
+ commit(types.pending)
+ return ComposeAPI.pageLayoutCreate(item, request.config(item)).then(pl => {
+ const pageLayout = new compose.PageLayout(pl)
+ commit(types.updateSet, [pageLayout])
+ return pageLayout
+ }).finally(() => {
+ commit(types.completed)
+ })
+ },
+
+ async update ({ commit }, item) {
+ commit(types.pending)
+ return ComposeAPI.pageLayoutUpdate(item, request.config(item)).then(pl => {
+ const pageLayout = new compose.PageLayout(pl)
+ commit(types.updateSet, [pageLayout])
+ return pageLayout
+ }).finally(() => {
+ commit(types.completed)
+ })
+ },
+
+ async delete ({ commit, dispatch }, item) {
+ commit(types.pending)
+ return ComposeAPI.pageLayoutDelete(item).then(() => {
+ commit(types.removeFromSet, [item])
+ return true
+ }).finally(() => {
+ commit(types.completed)
+ })
+ },
+
+ updateSet ({ commit }, pageLayout) {
+ commit(types.updateSet, [pageLayout])
+ },
+
+ clearSet ({ commit }) {
+ commit(types.clearSet)
+ },
+ },
+
+ mutations: {
+ [types.loading] (state) {
+ state.loading = true
+ },
+
+ [types.loaded] (state) {
+ state.loading = false
+ },
+
+ [types.pending] (state) {
+ state.pending = true
+ },
+
+ [types.completed] (state) {
+ state.pending = false
+ },
+
+ [types.updateSet] (state, set) {
+ set = set.map(i => Object.freeze(i))
+
+ if (state.set.length === 0) {
+ state.set = set
+ return
+ }
+
+ set.forEach(newItem => {
+ const oldIndex = state.set.findIndex(({ pageLayoutID }) => pageLayoutID === newItem.pageLayoutID)
+ if (oldIndex > -1) {
+ state.set.splice(oldIndex, 1, newItem)
+ } else {
+ state.set.push(newItem)
+ }
+ })
+ },
+
+ [types.removeFromSet] (state, removedSet) {
+ (removedSet || []).forEach(removedItem => {
+ const i = state.set.findIndex(({ pageLayoutID }) => pageLayoutID === removedItem.pageLayoutID)
+ if (i > -1) {
+ state.set.splice(i, 1)
+ }
+ })
+ },
+
+ [types.clearSet] (state) {
+ state.pending = false
+ state.set.splice(0)
+ },
+ },
+ }
+}
diff --git a/client/web/compose/src/views/Admin/Pages/Builder.vue b/client/web/compose/src/views/Admin/Pages/Builder.vue
index 141455bbc..17b44882f 100644
--- a/client/web/compose/src/views/Admin/Pages/Builder.vue
+++ b/client/web/compose/src/views/Admin/Pages/Builder.vue
@@ -1,6 +1,6 @@
-
+
+
@@ -134,13 +152,17 @@
id="createBlockSelector"
size="lg"
scrollable
- hide-footer
:title="$t('build.selectBlockTitle')"
>
+
+
+ {{ $t('block:selectBlockFootnote') }}
+
!this.blocks.some(b => b.blockID === blockID))
+ },
},
watch: {
@@ -418,16 +448,19 @@ export default {
immediate: true,
handler (pageID) {
this.page = undefined
+ this.layout = undefined
+ this.layouts = []
this.unsavedBlocks.clear()
if (pageID) {
const { namespaceID, name } = this.namespace
- this.findPageByID({ namespaceID, pageID, force: true })
- .then(page => {
- document.title = [page.title, name, this.$t('general:label.app-name.private')].filter(v => v).join(' | ')
-
- this.page = page.clone()
- })
+ this.findPageByID({ namespaceID, pageID, force: true }).then(page => {
+ document.title = [page.title, name, this.$t('general:label.app-name.private')].filter(v => v).join(' | ')
+ this.page = page.clone()
+ return this.fetchPageLayouts()
+ }).then(() => {
+ this.setLayout()
+ })
}
},
},
@@ -465,6 +498,9 @@ export default {
updatePageSet: 'page/updateSet',
createPage: 'page/create',
loadPages: 'page/load',
+ findLayoutByID: 'pageLayout/findByID',
+ findLayoutsByPageID: 'pageLayout/findByPageID',
+ updatePageLayout: 'pageLayout/update',
}),
fulfilEditRequest (blockID) {
@@ -538,23 +574,17 @@ export default {
}
this.blocks.splice(index, 1)
- this.page.blocks = this.blocks
if (this.editor) this.editor = undefined
this.unsavedBlocks.add(index)
},
- updatePageBlockGrid (blocks) {
- this.blocks = blocks
- },
-
onBlockUpdated (index) {
this.unsavedBlocks.add(index)
},
updateBlocks (block = this.editor.block) {
block = compose.PageBlockMaker(block)
- this.page.blocks = this.blocks
if (this.editor.index !== undefined) {
const oldBlock = this.blocks[this.editor.index]
@@ -565,24 +595,24 @@ export default {
}
if (this.editor.block.kind !== block.kind) {
- this.page.blocks.push(block)
+ this.blocks.push(block)
this.$root.$emit('builder-createRequestFulfilled', {
blockID: fetchID(block),
title: block.title,
})
} else {
- this.page.blocks.splice(this.editor.index, 1, block)
+ this.blocks.splice(this.editor.index, 1, block)
this.unsavedBlocks.add(this.editor.index)
}
} else {
- this.page.blocks.push(block)
- this.unsavedBlocks.add(this.page.blocks.length - 1)
+ this.blocks.push(block)
+ this.unsavedBlocks.add(this.blocks.length - 1)
}
if (block.kind === 'Tabs') {
block.options.tabs.forEach((tab) => {
if (!tab.blockID) return
- this.page.blocks.find(b => fetchID(b) === tab.blockID).meta.hidden = true
+ this.blocks.find(b => fetchID(b) === tab.blockID).meta.hidden = true
})
}
@@ -617,10 +647,22 @@ export default {
const maxY = this.blocks.filter(({ meta }) => !meta.hidden).map((block) => block.xywh[1]).reduce((acc, val) => {
return acc > val ? acc : val
}, 0)
- block.xywh = [0, maxY + 2, 3, 3]
+ block.xywh = [0, maxY + 2, 20, 15]
}
},
+ async fetchPageLayouts () {
+ const { namespaceID } = this.namespace
+
+ return this.findLayoutsByPageID({ namespaceID, pageID: this.pageID }).then(layouts => {
+ this.layouts = layouts.map(l => {
+ l = new compose.PageLayout(l)
+ l.label = l.meta.title || l.handle || l.pageLayoutID
+ return l
+ })
+ })
+ },
+
async handleSave ({ closeOnSuccess = false, previewOnSuccess = false } = {}) {
const { namespaceID } = this.namespace
@@ -651,25 +693,51 @@ export default {
this.processing = true
- this.findPageByID({ namespaceID, pageID: this.pageID, force: true })
- .then(page => {
- // Merge changes
- const mergedPage = new compose.Page({ namespaceID, ...page, blocks: this.blocks })
+ return Promise.all([
+ this.findPageByID({ ...this.page, force: true }),
+ this.findLayoutByID({ ...this.layout }),
+ ]).then(([page, layout]) => {
+ const blocks = [
+ ...page.blocks.filter(({ blockID }) => {
+ if (this.blocks.some(b => b.blockID === blockID)) {
+ return false
+ }
- return this.updatePage(mergedPage).then(this.updateTabbedBlockIDs)
- }).then((page) => {
- this.unsavedBlocks.clear()
- this.toastSuccess(this.$t('notification:page.saved'))
- if (closeOnSuccess) {
- this.$router.push({ name: 'admin.pages' })
- } else if (previewOnSuccess) {
- this.$router.push({ name: 'page', params: { pageID: this.pageID } })
- }
- this.page = new compose.Page(page)
- }).finally(() => {
- this.processing = false
- })
- .catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
+ // Check if block exists in any other layout, if not delete it permanently
+ return this.layouts.some(({ blocks }) => blocks.some(b => b.blockID === blockID))
+ }),
+ ...this.blocks,
+ ]
+
+ return this.updatePage({ namespaceID, ...page, blocks })
+ .then(this.updateTabbedBlockIDs)
+ .then(async page => {
+ const blocks = this.blocks.map(({ blockID, meta, xywh }) => {
+ if (blockID === NoID) {
+ blockID = (page.blocks.find(block => block.meta.tempID === meta.tempID) || {}).blockID
+ }
+
+ return { blockID, xywh }
+ })
+ layout = await this.updatePageLayout({ ...layout, blocks })
+ return { page, layout }
+ })
+ }).then(({ page, layout }) => {
+ this.page = new compose.Page(page)
+ this.layout = new compose.PageLayout(layout)
+ this.fetchPageLayouts()
+ this.$route.query.layoutID = layout.pageLayoutID
+ this.unsavedBlocks.clear()
+ this.toastSuccess(this.$t('notification:page.saved'))
+
+ if (closeOnSuccess) {
+ this.$router.push({ name: 'admin.pages' })
+ } else if (previewOnSuccess) {
+ this.$router.push({ name: 'page', params: { pageID: this.pageID } })
+ }
+ }).finally(() => {
+ this.processing = false
+ }).catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
},
validateModuleFieldSelection (module, page) {
@@ -732,7 +800,6 @@ export default {
navigator.clipboard.writeText(block).then(() => {
this.toastSuccess(this.$t('notification:page.copySuccess'))
- this.toastInfo(this.$t('notification:page.blockWaiting'))
this.$refs.pageBuilder.focus()
},
(err) => {
@@ -764,6 +831,38 @@ export default {
checkUnsavedBlocks (next) {
next(!this.unsavedBlocks.size || window.confirm(this.$t('build.unsavedChanges')))
},
+
+ 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.$router.replace({ ...this.$route, query: { ...this.$route.query, layoutID: this.layout.pageLayoutID } })
+ }
+
+ this.unsavedBlocks.clear()
+ const { blocks = [] } = this.layout || {}
+ this.blocks = blocks.map(({ blockID, xywh }) => {
+ const block = this.page.blocks.find(b => fetchID(b) === blockID)
+ block.xywh = xywh
+ return block
+ })
+ },
+
+ switchLayout (layoutID) {
+ this.$router.push({ ...this.$route, query: { ...this.$route.query, layoutID } }).then(() => {
+ this.setLayout()
+ }).catch(() => {
+ // Change layout value of select back to previous one if redirect was canceled
+ this.$refs.layoutSelect.localValue = this.layout.pageLayoutID
+ })
+ },
},
}
diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue
index d8a49a4bc..15b7e32ac 100644
--- a/client/web/compose/src/views/Admin/Pages/Edit.vue
+++ b/client/web/compose/src/views/Admin/Pages/Edit.vue
@@ -1,5 +1,8 @@
-
+
{{ $t('edit.edit') }}
@@ -17,7 +20,7 @@
>
{{ $t('label.pageBuilder') }}
@@ -44,142 +47,297 @@
-
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+ {{ $t('block.general.invalid-handle-characters') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('icon.page') }}
+
-
-
+
+
+
+
+
+
+ {{ $t('icon.noIcon') }}
+
+
+
+
+
+
+
+ {{ $t('edit.visible') }}
+
+
+
+ {{ $t('showSubPages') }}
+
+
+
+
+
+
+
+
+
+
+ |
+
+
-
-
-
+ Title *
+ |
-
-
-
-
- {{ $t('block.general.invalid-handle-characters') }}
-
-
-
+
+ Handle
+ |
-
-
-
-
-
+ |
+
+
-
-
-
- {{ $t('icon.page') }}
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{ $t('icon.noIcon') }}
-
-
-
-
-
-
-
- {{ $t('edit.visible') }}
-
-
-
- {{ $t('showSubPages') }}
-
-
-
-
-
-
-
-
+
+ |
+
+
+
+
+ Add layout
+
+
+
+
+
+
+
+
+
+
+ ƒ
+
+
+
+
+
+
+
+
+
+
+
selfID === this.page.pageID)
+ return this.page ? this.pages.some(({ selfID }) => selfID === this.page.pageID) : false
},
disableSave () {
- return [this.titleState, this.handleState].includes(false)
+ return [this.titleState, this.handleState].includes(false) || this.layouts.some(l => !l.meta.title || handle.handleState(l.handle) === false)
},
hideDelete () {
@@ -417,7 +592,7 @@ export default {
},
set (icon) {
- this.page.config.navItem.icon = icon
+ this.$set(this.page.config.navItem, 'icon', icon)
},
},
@@ -426,24 +601,54 @@ export default {
},
pageIcon () {
+ if (!this.icon.src) {
+ return
+ }
+
return this.icon.type === 'link' ? this.icon.src : this.makeAttachmentUrl(this.icon.src)
},
+
+ currentLayoutRoles: {
+ get () {
+ if (!this.layoutEditor.layout) {
+ return []
+ }
+
+ return this.layoutEditor.layout.config.visibility.roles
+ },
+
+ set (roles) {
+ this.$set(this.layoutEditor.layout.config.visibility, 'roles', roles)
+ },
+ },
},
watch: {
pageID: {
immediate: true,
handler (pageID) {
+ this.page = undefined
+ this.layouts = []
+
+ this.deletedLayouts = new Set()
+
if (pageID) {
- this.findPageByID({ namespaceID: this.namespace.namespaceID, pageID }).then((page) => {
+ const { namespaceID } = this.namespace
+ this.findPageByID({ namespaceID, pageID }).then((page) => {
this.page = page.clone()
return this.fetchAttachments()
}).catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
+
+ this.fetchLayouts({ namespaceID, pageID }).catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
}
},
},
},
+ created () {
+ this.fetchRoles()
+ },
+
methods: {
...mapActions({
findPageByID: 'page/findByID',
@@ -451,23 +656,86 @@ export default {
deletePage: 'page/delete',
createPage: 'page/create',
loadPages: 'page/load',
+ findLayoutsByPageID: 'pageLayout/findByPageID',
+ createPageLayout: 'pageLayout/create',
+ updatePageLayout: 'pageLayout/update',
+ deletePageLayout: 'pageLayout/delete',
}),
- async handleSave ({ closeOnSuccess = false } = {}) {
+ async fetchLayouts (payload) {
+ return this.findLayoutsByPageID(payload).then(layouts => {
+ this.layouts = layouts.map(layout => new compose.PageLayout(layout))
+ })
+ },
+
+ async fetchRoles () {
+ this.roles.processing = true
+
+ this.$SystemAPI.roleList().then(({ set: roles = [] }) => {
+ this.roles.options = roles.filter(({ meta }) => !(meta.context && meta.context.resourceTypes))
+ }).finally(() => {
+ this.roles.processing = false
+ })
+ },
+
+ addLayout () {
+ this.layouts.push(new compose.PageLayout({ namespaceID: this.namespace.namespaceID, pageID: this.pageID }))
+ },
+
+ updateLayout () {
+ this.layoutEditor.layout.meta.updated = true
+ this.layouts.splice(this.layoutEditor.index, 1, this.layoutEditor.layout)
+ this.layoutEditor.index = undefined
+ this.layoutEditor.layout = undefined
+ },
+
+ deleteLayout (index) {
+ const { pageLayoutID } = this.layouts[index] || {}
+ if (pageLayoutID !== NoID) {
+ this.deletedLayouts.add(this.layouts[index])
+ }
+
+ this.layouts.splice(index, 1)
+ },
+
+ configureLayout (index) {
+ this.layoutEditor.index = index
+ this.layoutEditor.layout = { ...this.layouts[index] }
+ },
+
+ async handleSaveLayouts () {
+ return Promise.all([
+ ...[...this.deletedLayouts].map(this.deletePageLayout),
+ ...this.layouts.map(layout => {
+ if (layout.pageLayoutID === NoID) {
+ return this.createPageLayout(layout)
+ } else if (layout.meta.updated) {
+ return this.updatePageLayout(layout)
+ }
+ }),
+ ])
+ },
+
+ handleSave ({ closeOnSuccess = false } = {}) {
/**
* Pass a special tag alongside payload that
* instructs store layer to add content-language header to the API request
*/
const resourceTranslationLanguage = this.currentLanguage
+ const { namespaceID } = this.namespace
return this.saveIcon().then(icon => {
this.page.config.navItem.icon = icon
- return this.updatePage({ namespaceID: this.namespace.namespaceID, ...this.page, resourceTranslationLanguage }).then((page) => {
+ return this.updatePage({ namespaceID, ...this.page, resourceTranslationLanguage }).then((page) => {
this.page = page.clone()
- this.toastSuccess(this.$t('notification:page.saved'))
- if (closeOnSuccess) {
- this.$router.push({ name: 'admin.pages' })
- }
+ return this.handleSaveLayouts()
})
+ }).then(() => {
+ this.deletedLayouts = new Set()
+
+ this.toastSuccess(this.$t('notification:page.saved'))
+ if (closeOnSuccess) {
+ this.$router.push({ name: 'admin.pages' })
+ }
}).catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
},
@@ -541,6 +809,13 @@ export default {
return `${this.$ComposeAPI.baseURL}${src}`
},
+ layoutTitleState (title) {
+ return title ? null : false
+ },
+
+ layoutHandleState (layoutHandle) {
+ return handle.handleState(layoutHandle)
+ },
},
}
diff --git a/client/web/compose/src/views/Admin/Pages/List.vue b/client/web/compose/src/views/Admin/Pages/List.vue
index 8bdd9e6ea..95c123e80 100644
--- a/client/web/compose/src/views/Admin/Pages/List.vue
+++ b/client/web/compose/src/views/Admin/Pages/List.vue
@@ -131,6 +131,7 @@ export default {
methods: {
...mapActions({
createPage: 'page/create',
+ createPageLayout: 'pageLayout/create',
}),
loadTree () {
@@ -148,7 +149,10 @@ export default {
const { namespaceID } = this.namespace
this.page.weight = this.tree.length
this.createPage({ ...this.page, namespaceID }).then(({ pageID }) => {
- this.$router.push({ name: 'admin.pages.edit', params: { pageID } })
+ const pageLayout = new compose.PageLayout({ namespaceID, pageID })
+ return this.createPageLayout(pageLayout).then(() => {
+ this.$router.push({ name: 'admin.pages.edit', params: { pageID } })
+ })
}).catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
},
diff --git a/client/web/compose/src/views/Namespace/Edit.vue b/client/web/compose/src/views/Namespace/Edit.vue
index a1bcd9bf8..1772fc7c9 100644
--- a/client/web/compose/src/views/Namespace/Edit.vue
+++ b/client/web/compose/src/views/Namespace/Edit.vue
@@ -32,7 +32,7 @@
style="margin-left:2px;"
>
{
this.loaded = true
})
diff --git a/client/web/compose/src/views/Public/Pages/View.vue b/client/web/compose/src/views/Public/Pages/View.vue
index 5d828b1d6..0c123e3ca 100644
--- a/client/web/compose/src/views/Public/Pages/View.vue
+++ b/client/web/compose/src/views/Public/Pages/View.vue
@@ -21,7 +21,7 @@
>
{{ $t('general:label.pageBuilder') }}
diff --git a/client/web/reporter/src/components/Report/Grid.vue b/client/web/reporter/src/components/Report/Grid.vue
index 594f78e3f..6eaa0285b 100644
--- a/client/web/reporter/src/components/Report/Grid.vue
+++ b/client/web/reporter/src/components/Report/Grid.vue
@@ -63,7 +63,7 @@ export default {
props: {
blocks: {
type: Array,
- default: () => [],
+ default: () => ([]),
},
editable: {
@@ -110,14 +110,6 @@ export default {
},
},
},
-
- mounted () {
- window.addEventListener('resize', this.windowResizeThrottledHandler)
- },
-
- destroyed () {
- window.removeEventListener('resize', this.windowResizeThrottledHandler)
- },
}
diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts
index 8ef8b4260..218f142dc 100644
--- a/lib/js/src/api-clients/compose.ts
+++ b/lib/js/src/api-clients/compose.ts
@@ -555,6 +555,7 @@ export default class Compose {
visible,
blocks,
config,
+ meta,
} = (a as KV) || {}
if (!namespaceID) {
throw Error('field namespaceID is empty')
@@ -580,6 +581,7 @@ export default class Compose {
visible,
blocks,
config,
+ meta,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
@@ -663,6 +665,7 @@ export default class Compose {
visible,
blocks,
config,
+ meta,
} = (a as KV) || {}
if (!namespaceID) {
throw Error('field namespaceID is empty')
@@ -691,6 +694,7 @@ export default class Compose {
visible,
blocks,
config,
+ meta,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
@@ -1112,6 +1116,7 @@ export default class Compose {
namespaceID,
pageID,
parentID,
+ weight,
moduleID,
handle,
primary,
@@ -1136,6 +1141,7 @@ export default class Compose {
}
cfg.data = {
parentID,
+ weight,
moduleID,
handle,
primary,
@@ -1199,6 +1205,7 @@ export default class Compose {
pageID,
pageLayoutID,
parentID,
+ weight,
moduleID,
handle,
primary,
@@ -1226,6 +1233,7 @@ export default class Compose {
}
cfg.data = {
parentID,
+ weight,
moduleID,
handle,
primary,
@@ -1247,6 +1255,43 @@ export default class Compose {
return `/namespace/${namespaceID}/page/${pageID}/layout/${pageLayoutID}`
}
+ // Reorder page layouts
+ async pageLayoutReorder (a: KV, extra: AxiosRequestConfig = {}): Promise {
+ const {
+ namespaceID,
+ pageID,
+ pageIDs,
+ } = (a as KV) || {}
+ if (!namespaceID) {
+ throw Error('field namespaceID is empty')
+ }
+ if (!pageID) {
+ throw Error('field pageID is empty')
+ }
+ if (!pageIDs) {
+ throw Error('field pageIDs is empty')
+ }
+ const cfg: AxiosRequestConfig = {
+ ...extra,
+ method: 'post',
+ url: this.pageLayoutReorderEndpoint({
+ namespaceID, pageID,
+ }),
+ }
+ cfg.data = {
+ pageIDs,
+ }
+ return this.api().request(cfg).then(result => stdResolve(result))
+ }
+
+ pageLayoutReorderEndpoint (a: KV): string {
+ const {
+ namespaceID,
+ pageID,
+ } = a || {}
+ return `/namespace/${namespaceID}/page/${pageID}/layout/reorder`
+ }
+
// Delete page layout
async pageLayoutDelete (a: KV, extra: AxiosRequestConfig = {}): Promise {
const {
diff --git a/lib/js/src/compose/index.ts b/lib/js/src/compose/index.ts
index d8507f433..f913029f3 100644
--- a/lib/js/src/compose/index.ts
+++ b/lib/js/src/compose/index.ts
@@ -4,6 +4,7 @@ export * from './types/revision'
export * from './types/module-field'
export { Namespace } from './types/namespace'
export { Page } from './types/page'
+export { PageLayout } from './types/page-layout'
export * from './types/page-block'
export { RecordValidator } from './validators/record'
export { getModuleFromYaml } from './helpers'
diff --git a/lib/js/src/compose/types/module.ts b/lib/js/src/compose/types/module.ts
index f5ad7a6e0..bd27e001b 100644
--- a/lib/js/src/compose/types/module.ts
+++ b/lib/js/src/compose/types/module.ts
@@ -235,8 +235,6 @@ export class Module {
if (IsOf(m, 'config')) {
this.config = merge({}, this.config, m.config)
-
- // Remove when we improve duplicate detection, for now its always enabled
}
if (IsOf(m, 'labels')) {
diff --git a/lib/js/src/compose/types/page-block/base.ts b/lib/js/src/compose/types/page-block/base.ts
index 614bedefe..5b2cf9ad8 100644
--- a/lib/js/src/compose/types/page-block/base.ts
+++ b/lib/js/src/compose/types/page-block/base.ts
@@ -27,16 +27,18 @@ interface PageBlockMeta {
export type PageBlockInput = PageBlock | Partial
-const defaultXYWH = [0, 2000, 3, 3]
+const defaultXYWH = [0, 0, 20, 15]
export class PageBlock {
+ // blockID is auto generated by the server in order to support resource translations
+ public blockID = NoID;
+ public kind = ''
+
public title = '';
public description = '';
- // blockID is auto generated by the server in order to support resource translations
- public blockID = '0';
- xywh: number[] = defaultXYWH
- kind = ''
+ public xywh: number[] = defaultXYWH
+
public options = {}
diff --git a/lib/js/src/compose/types/page-layout.ts b/lib/js/src/compose/types/page-layout.ts
new file mode 100644
index 000000000..029aba9e2
--- /dev/null
+++ b/lib/js/src/compose/types/page-layout.ts
@@ -0,0 +1,106 @@
+import { merge } from 'lodash'
+import { Apply, CortezaID, ISO8601Date, NoID } from '../../cast'
+import { PageBlock } from './page-block/base'
+import { Button } from './page-block/types'
+
+export type PageLayoutInput = PageLayout | Partial
+
+interface PageLayoutConfig {
+ visibility: Visibility;
+ actions: Action[];
+}
+
+interface Action {
+ actionID: string;
+ kind: string;
+ placement: string;
+ params: unknown;
+ meta: unknown;
+}
+
+interface Visibility {
+ expression: string;
+ roles: string[];
+}
+
+interface Meta {
+ title: string;
+ description: string;
+}
+
+export class PageLayout {
+ public pageLayoutID = NoID;
+ public namespaceID = NoID;
+ public pageID = NoID
+ public handle = '';
+
+ public weight = 0;
+
+ public blocks: (Partial)[] = [];
+
+ public config: PageLayoutConfig = {
+ visibility: {
+ expression: '',
+ roles: [],
+ },
+ actions: [],
+ }
+
+ public meta: Meta = {
+ title: '',
+ description: ''
+ };
+
+ public createdAt?: Date = undefined;
+ public updatedAt?: Date = undefined;
+ public deletedAt?: Date = undefined;
+
+ public ownedBy = NoID;
+
+ constructor (pl?: PageLayoutInput) {
+ this.apply(pl)
+ }
+
+ apply (pl?: PageLayoutInput): void {
+ if (!pl) return
+
+ Apply(this, pl, CortezaID, 'pageLayoutID', 'namespaceID', 'pageID', 'ownedBy')
+ Apply(this, pl, String, 'handle')
+ Apply(this, pl, ISO8601Date, 'createdAt', 'updatedAt', 'deletedAt')
+
+ this.blocks = (pl.blocks || []).map(({ blockID, xywh }) => ({ blockID, xywh }))
+
+ if (pl.meta) {
+ this.meta = { ...this.meta, ...pl.meta }
+ }
+
+ if (pl.config) {
+ this.config = merge({}, this.config, pl.config)
+ }
+ }
+
+ clone (): PageLayout {
+ return new PageLayout(JSON.parse(JSON.stringify(this)))
+ }
+
+ /**
+ * Returns resource ID
+ */
+ get resourceID (): string {
+ return `${this.resourceType}:${this.pageLayoutID}`
+ }
+
+ /**
+ * Resource type
+ */
+ get resourceType (): string {
+ return 'compose:page-layout'
+ }
+
+ export (): PageLayoutInput {
+ return {
+ blocks: this.blocks,
+ meta: this.meta,
+ }
+ }
+}
diff --git a/lib/js/src/compose/types/page.ts b/lib/js/src/compose/types/page.ts
index aaa279d9f..9d61a9fbc 100644
--- a/lib/js/src/compose/types/page.ts
+++ b/lib/js/src/compose/types/page.ts
@@ -1,12 +1,14 @@
import { Apply, CortezaID, ISO8601Date, NoID } from '../../cast'
import { IsOf, AreObjectsOf } from '../../guards'
import { PageBlock, PageBlockMaker } from './page-block'
-import { Button } from './page-block/types'
+import { merge } from 'lodash'
-interface PartialPage extends Partial> {
+interface PartialPage extends Partial> {
children?: Array;
- blocks?: (Partial)[];
+ blocks?: PageBlock[];
+
+ meta?: object;
createdAt?: string|number|Date;
updatedAt?: string|number|Date;
@@ -14,15 +16,6 @@ interface PartialPage extends Partial
+ public children?: Page[];
- public blocks: (InstanceType)[] = [];
+ public blocks: PageBlock[] = [];
public config: PageConfig = {
- buttons: {
- submit: { enabled: true },
- delete: { enabled: true },
- new: { enabled: true },
- edit: { enabled: true },
- clone: { enabled: true },
- back: { enabled: true },
- },
- attachments: [],
navItem: {
icon: {
type: '',
@@ -69,6 +53,7 @@ export class Page {
expanded: false,
},
}
+ public meta: object = {};
public createdAt?: Date = undefined;
public updatedAt?: Date = undefined;
@@ -95,10 +80,7 @@ export class Page {
Apply(this, i, Boolean, 'visible')
if (i.blocks) {
- this.blocks = []
- if (AreObjectsOf(i.blocks, 'kind') && AreObjectsOf(i.blocks, 'xywh')) {
- this.blocks = i.blocks.map((b: { kind: string }) => PageBlockMaker(b))
- }
+ this.blocks = i.blocks.map(block => PageBlockMaker(block))
}
if (i.children) {
@@ -108,8 +90,12 @@ export class Page {
}
}
- if (i.config) {
- this.config = i.config
+ if (IsOf(i, 'config')) {
+ this.config = merge({}, this.config, i.config)
+ }
+
+ if (IsOf(i, 'meta')) {
+ this.meta = merge({}, this.meta, i.meta)
}
if (IsOf(i, 'labels')) {