Add base multi page layout implementation
This commit is contained in:
@@ -112,6 +112,7 @@ export default {
|
||||
...mapActions({
|
||||
createPage: 'page/create',
|
||||
updatePage: 'page/update',
|
||||
createPageLayout: 'pageLayout/create',
|
||||
}),
|
||||
|
||||
handleRecordPageCreation () {
|
||||
@@ -121,19 +122,21 @@ export default {
|
||||
const { namespaceID } = this.namespace
|
||||
|
||||
// A simple record block w/o preselected fields
|
||||
const blocks = [new compose.PageBlockRecord({ xywh: [0, 0, 12, 16] })]
|
||||
const blocks = [new compose.PageBlockRecord({ xywh: [0, 0, 48, 82] })]
|
||||
const selfID = (this.recordListPage || {}).pageID || NoID
|
||||
|
||||
const page = {
|
||||
const page = new compose.Page({
|
||||
namespaceID,
|
||||
moduleID,
|
||||
selfID,
|
||||
title: `${this.$t('forModule.recordPage')} "${name || moduleID}"`,
|
||||
blocks,
|
||||
}
|
||||
})
|
||||
|
||||
this.createPage(page)
|
||||
.catch(this.toastErrorHandler(this.$t('notification:module.recordPage.createFailed')))
|
||||
this.createPage(page).then(({ pageID, blocks }) => {
|
||||
const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title: 'Primary' } })
|
||||
return this.createPageLayout(pageLayout)
|
||||
}).catch(this.toastErrorHandler(this.$t('notification:module.recordPage.createFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
@@ -146,7 +149,7 @@ export default {
|
||||
const { name, moduleID } = this.module
|
||||
|
||||
const blocks = [new compose.PageBlockRecordList({
|
||||
xywh: [0, 0, 12, 17],
|
||||
xywh: [0, 0, 48, 82],
|
||||
options: {
|
||||
moduleID,
|
||||
fields: [],
|
||||
@@ -156,15 +159,19 @@ export default {
|
||||
},
|
||||
})]
|
||||
|
||||
const page = {
|
||||
const page = new compose.Page({
|
||||
title: `${this.$t('forModule.recordList')} "${name || moduleID}"`,
|
||||
namespaceID,
|
||||
blocks,
|
||||
}
|
||||
})
|
||||
|
||||
this.createPage(page)
|
||||
.then(({ pageID: selfID = NoID }) => {
|
||||
return this.updatePage({ ...this.recordPage, selfID })
|
||||
.then(({ pageID = NoID, blocks }) => {
|
||||
const pageLayout = new compose.PageLayout({ namespaceID, pageID, blocks, meta: { title: 'Primary' } })
|
||||
return Promise.all([
|
||||
this.updatePage({ ...this.recordPage, selfID: pageID }),
|
||||
this.createPageLayout(pageLayout),
|
||||
])
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:module.recordPage.createFailed')))
|
||||
.finally(() => {
|
||||
|
||||
@@ -2,55 +2,101 @@
|
||||
<b-container fluid>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="4"
|
||||
cols="12"
|
||||
>
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="(type) in types"
|
||||
:key="type.label"
|
||||
:disabled="isOptionDisabled(type)"
|
||||
button
|
||||
@click="$emit('select', type.block)"
|
||||
@mouseover="current = type.image"
|
||||
>
|
||||
{{ type.label }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
<b-button
|
||||
v-for="(type) in types"
|
||||
:key="type.label"
|
||||
:disabled="isOptionDisabled(type)"
|
||||
variant="outline-light"
|
||||
class="mr-2 mb-2 text-dark"
|
||||
@click="$emit('select', type.block)"
|
||||
@mouseover="current = type.image"
|
||||
>
|
||||
{{ type.label }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="8"
|
||||
class="my-auto"
|
||||
cols="12"
|
||||
>
|
||||
<b-img
|
||||
v-if="current"
|
||||
fluid
|
||||
thumbnail
|
||||
:src="current"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row
|
||||
class="border-top mt-2"
|
||||
>
|
||||
<b-col>
|
||||
<div
|
||||
class="mt-2"
|
||||
class="d-flex"
|
||||
style="height: 300px"
|
||||
>
|
||||
{{ $t('selectBlockFootnote') }}
|
||||
<b-img
|
||||
v-if="current"
|
||||
:src="current"
|
||||
center
|
||||
fluid
|
||||
class="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
|
||||
<hr
|
||||
v-if="existingBlocks.length"
|
||||
class="w-100"
|
||||
>
|
||||
|
||||
<b-col
|
||||
v-if="existingBlocks.length"
|
||||
cols="12"
|
||||
>
|
||||
<b-input-group class="d-flex w-100">
|
||||
<vue-select
|
||||
v-model="selectedExistingBlock"
|
||||
:get-option-label="getBlockLabel"
|
||||
:options="existingBlocks"
|
||||
:calculate-position="calculateDropdownPosition"
|
||||
placeholder="Blocks from other layouts"
|
||||
append-to-body
|
||||
class="block-selector bg-white position-relative"
|
||||
/>
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
title="Clone block without reference"
|
||||
variant="light"
|
||||
:disabled="!selectedExistingBlock"
|
||||
class="d-flex align-items-center"
|
||||
@click="$emit('select', selectedExistingBlock.clone())"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'clone']"
|
||||
/>
|
||||
</b-button>
|
||||
<b-button
|
||||
title="Copy block with references"
|
||||
variant="light"
|
||||
:disabled="!selectedExistingBlock"
|
||||
class="d-flex align-items-center"
|
||||
@click="$emit('select', selectedExistingBlock)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'copy']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
import * as images from '../../../../assets/PageBlocks'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'block',
|
||||
},
|
||||
|
||||
components: {
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
recordPage: {
|
||||
type: Boolean,
|
||||
@@ -61,11 +107,19 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
existingBlocks: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
current: undefined,
|
||||
|
||||
selectedExistingBlock: undefined,
|
||||
|
||||
types: [
|
||||
{
|
||||
label: this.$t('automation.label'),
|
||||
@@ -172,6 +226,50 @@ export default {
|
||||
isOptionDisabled (type) {
|
||||
return (!this.recordPage && type.recordPageOnly) || this.disabledKinds.includes(type.block.kind)
|
||||
},
|
||||
|
||||
getBlockLabel ({ title, kind }) {
|
||||
return title || kind
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.block-selector {
|
||||
position: relative;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.block-selector {
|
||||
&:not(.vs--open) .vs__selected + .vs__search {
|
||||
// force this to not use any space
|
||||
// we still need it to be rendered for the focus
|
||||
width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.vs__selected-options {
|
||||
// do not allow growing
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.vs__selected {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-menu .vs__dropdown-option {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="grid.length"
|
||||
v-if="layout.length"
|
||||
class="w-100"
|
||||
:class="{
|
||||
editable: !!editable,
|
||||
'editable': editable,
|
||||
'flex-grow-1 d-flex': isStretchable,
|
||||
}"
|
||||
>
|
||||
<grid-layout
|
||||
class="flex-grow-1 d-flex w-100 h-100"
|
||||
:layout.sync="layout"
|
||||
:row-height="50"
|
||||
:is-resizable="!!editable"
|
||||
:is-draggable="!!editable"
|
||||
:responsive="!editable"
|
||||
:col-num="48"
|
||||
:row-height="10"
|
||||
vertical-compact
|
||||
:is-resizable="editable"
|
||||
:is-draggable="editable"
|
||||
:cols="columnNumber"
|
||||
:margin="[0, 0]"
|
||||
:use-css-transforms="false"
|
||||
@layout-updated="handleLayoutUpdate"
|
||||
:responsive="!editable"
|
||||
use-css-transforms
|
||||
class="flex-grow-1 d-flex w-100 h-100"
|
||||
>
|
||||
<template
|
||||
v-for="(item, index) in gridCollection"
|
||||
v-for="(item, index) in layout"
|
||||
>
|
||||
<grid-item
|
||||
v-if="!blocks[item.i].meta.hidden"
|
||||
:key="item.i"
|
||||
ref="items"
|
||||
:i="item.i"
|
||||
:h="item.h"
|
||||
:w="item.w"
|
||||
:x="item.x"
|
||||
:y="item.y"
|
||||
:min-w="6"
|
||||
:min-h="5"
|
||||
:class="{ 'h-100': isStretchable }"
|
||||
class="grid-item"
|
||||
:class="{
|
||||
'h-100': isStretchable,
|
||||
}"
|
||||
style="touch-action: none;"
|
||||
v-bind="{ ...item }"
|
||||
@moved="onBlockUpdated(index)"
|
||||
@resized="onBlockUpdated(index)"
|
||||
>
|
||||
@@ -46,6 +51,7 @@
|
||||
</template>
|
||||
</grid-layout>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="no-builder-grid h-100 pt-5 container text-center"
|
||||
@@ -57,9 +63,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueGridLayout from 'vue-grid-layout'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
import { GridLayout, GridItem } from 'vue-grid-layout'
|
||||
import { throttle } from 'lodash'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
@@ -67,25 +73,25 @@ export default {
|
||||
},
|
||||
|
||||
components: {
|
||||
GridLayout: VueGridLayout.GridLayout,
|
||||
GridItem: VueGridLayout.GridItem,
|
||||
GridLayout,
|
||||
GridItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
editable: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
blocks: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
editable: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// all blocks in vue-grid friendly structure
|
||||
grid: [],
|
||||
layout: [],
|
||||
|
||||
// Grid items bounding rect info
|
||||
boundingRects: [],
|
||||
@@ -93,65 +99,19 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
layout: {
|
||||
get () {
|
||||
return this.grid
|
||||
},
|
||||
|
||||
set (layout) {
|
||||
this.grid = layout
|
||||
this.handleLayoutUpdate(layout)
|
||||
},
|
||||
},
|
||||
|
||||
sortedGrid () {
|
||||
return Array.from(this.grid).sort((a, b) => Math.sqrt(a.x * a.x + a.y * a.y) - Math.sqrt(b.x * b.x + b.y * b.y))
|
||||
oneBlockLayout () {
|
||||
return this.blocks.filter(({ meta }) => !meta.hidden).length === 1
|
||||
},
|
||||
|
||||
isStretchable () {
|
||||
if (this.editable) {
|
||||
// When in-edit mode do not stretch the blocks
|
||||
return false
|
||||
}
|
||||
|
||||
const minHeight = 10
|
||||
let heightCheck = -1
|
||||
|
||||
for (let b = 0; b < this.blocks.length; b++) {
|
||||
const { xywh: [, y, , h] } = this.blocks[b]
|
||||
|
||||
if (y > 0) {
|
||||
// If block is not positioned at the top,
|
||||
// do not try to make it stretchable
|
||||
return false
|
||||
}
|
||||
|
||||
if (heightCheck === -1) {
|
||||
// Set block height for the next check
|
||||
heightCheck = h
|
||||
}
|
||||
|
||||
if (heightCheck !== h && minHeight > h) {
|
||||
// Not full height
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return !this.editable && this.oneBlockLayout
|
||||
},
|
||||
|
||||
columnNumber () {
|
||||
if (this.grid.length === 1) {
|
||||
if (this.oneBlockLayout) {
|
||||
return { lg: 1, md: 1, sm: 1, xs: 1, xxs: 1 }
|
||||
}
|
||||
return { lg: 12, md: 12, sm: 1, xs: 1, xxs: 1 }
|
||||
},
|
||||
|
||||
gridCollection () {
|
||||
if (this.grid.length === 1) {
|
||||
return this.sortedGrid
|
||||
}
|
||||
return this.grid
|
||||
return { lg: 48, md: 48, sm: 1, xs: 1, xxs: 1 }
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,8 +120,7 @@ export default {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (blocks) {
|
||||
if (blocks.length === 0) this.$emit('change', [])
|
||||
this.grid = blocks.map(({ meta, xywh: [x, y, w, h] }, i) => {
|
||||
this.layout = blocks.map(({ meta, xywh: [x, y, w, h] }, i) => {
|
||||
// To avoid collision with hidden elements
|
||||
return meta.hidden ? { i, x: 0, y: 0, w: 0, h: 0 } : { i, x, y, w, h }
|
||||
})
|
||||
@@ -190,16 +149,11 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
handleLayoutUpdate (layout) {
|
||||
this.$emit('change', layout.map(
|
||||
({ x, y, w, h, i }) => new compose.PageBlockMaker({ ...this.blocks[i], xywh: [x, y, w, h] }),
|
||||
))
|
||||
this.recalculateBoundingRect()
|
||||
},
|
||||
|
||||
// emit event when block has been moved or resized
|
||||
onBlockUpdated (index) {
|
||||
this.$emit('item-updated', index)
|
||||
this.$emit('update:blocks', this.layout.map(
|
||||
({ x, y, w, h, i }) => new compose.PageBlockMaker({ ...this.blocks[i], xywh: [x, y, w, h] }),
|
||||
))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -829,7 +829,7 @@ export default {
|
||||
},
|
||||
|
||||
/*
|
||||
Inline record editor is disabled if:
|
||||
Inline record editor is disabled if:
|
||||
- An inline record editor for the same module already exists
|
||||
- Record list module doesn't have record page (inline record autoselected and disabled)
|
||||
*/
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<script>
|
||||
import base from './base'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/block'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
import base from './base'
|
||||
import draggable from 'vuedraggable'
|
||||
import { VueSelect } from 'vue-select'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/block'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<grid
|
||||
v-if="page.blocks"
|
||||
:key="page.pageID"
|
||||
:blocks="page.blocks"
|
||||
v-if="layout"
|
||||
:key="layout.layoutID"
|
||||
:blocks="blocks"
|
||||
:editable="false"
|
||||
>
|
||||
<template
|
||||
@@ -17,6 +17,7 @@
|
||||
</grid>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Grid from '../../Common/Grid'
|
||||
import PageBlock from '../../PageBlocks'
|
||||
import { compose } from '@cortezaproject/corteza-js'
|
||||
@@ -35,5 +36,43 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
layouts: [],
|
||||
layout: undefined,
|
||||
|
||||
blocks: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getPageLayouts: 'pageLayout/getByPageID',
|
||||
}),
|
||||
},
|
||||
|
||||
watch: {
|
||||
'page.pageID': {
|
||||
immediate: true,
|
||||
handler (pageID) {
|
||||
this.layouts = this.getPageLayouts(pageID)
|
||||
const { layoutID } = this.$route.query
|
||||
|
||||
if (layoutID) {
|
||||
this.layout = this.layouts.find(({ pageLayoutID }) => pageLayoutID === layoutID)
|
||||
} else {
|
||||
this.layout = this.layouts[0]
|
||||
this.$router.replace({ ...this.$route, query: { ...this.$route.query, layoutID: this.layout.pageLayoutID } })
|
||||
}
|
||||
|
||||
this.blocks = (this.layout || {}).blocks.map(({ blockID, xywh }) => {
|
||||
const block = this.page.blocks.find(b => b.blockID === blockID)
|
||||
block.xywh = xywh
|
||||
return block
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
194
client/web/compose/src/store/page-layout.js
Normal file
194
client/web/compose/src/store/page-layout.js
Normal file
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!!page"
|
||||
v-if="page"
|
||||
id="page-builder"
|
||||
ref="pageBuilder"
|
||||
class="flex-grow-1 overflow-auto d-flex px-2 w-100"
|
||||
@@ -11,10 +11,22 @@
|
||||
</portal>
|
||||
|
||||
<portal to="topbar-tools">
|
||||
<b-button-group
|
||||
v-if="page && page.canUpdatePage"
|
||||
<b-form-select
|
||||
v-if="layout"
|
||||
ref="layoutSelect"
|
||||
size="sm"
|
||||
class="mr-1"
|
||||
:value="layout.pageLayoutID"
|
||||
value-field="pageLayoutID"
|
||||
text-field="label"
|
||||
:options="layouts"
|
||||
style="width: 300px;"
|
||||
@change="switchLayout"
|
||||
/>
|
||||
|
||||
<b-button-group
|
||||
v-if="page.canUpdatePage"
|
||||
size="sm"
|
||||
class="ml-2 text-nowrap"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
@@ -47,9 +59,8 @@
|
||||
</portal>
|
||||
|
||||
<grid
|
||||
:blocks="page.blocks"
|
||||
:blocks.sync="blocks"
|
||||
editable
|
||||
@change="updatePageBlockGrid"
|
||||
@item-updated="onBlockUpdated"
|
||||
>
|
||||
<template
|
||||
@@ -58,7 +69,6 @@
|
||||
<div
|
||||
:data-test-id="`block-${block.kind}`"
|
||||
class="h-100 editable-block"
|
||||
:class="{ 'bg-warning': !isValid(block) }"
|
||||
>
|
||||
<div
|
||||
class="toolbox border-0 p-2 m-0 text-light text-center"
|
||||
@@ -121,9 +131,17 @@
|
||||
</div>
|
||||
|
||||
<page-block
|
||||
v-bind="{ ...$attrs, ...$props, page, block, boundingRect, blockIndex: index, editable: true }"
|
||||
:record="record"
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...$props
|
||||
}"
|
||||
:page="page"
|
||||
:module="module"
|
||||
:record="record"
|
||||
:block-index="index"
|
||||
:block="block"
|
||||
:bounding-rect="boundingRect"
|
||||
editable
|
||||
class="p-2"
|
||||
/>
|
||||
</div>
|
||||
@@ -134,13 +152,17 @@
|
||||
id="createBlockSelector"
|
||||
size="lg"
|
||||
scrollable
|
||||
hide-footer
|
||||
:title="$t('build.selectBlockTitle')"
|
||||
>
|
||||
<new-block-selector
|
||||
:record-page="!!module"
|
||||
:existing-blocks="selectableExistingBlocks"
|
||||
@select="addBlock"
|
||||
/>
|
||||
|
||||
<template #modal-footer>
|
||||
{{ $t('block:selectBlockFootnote') }}
|
||||
</template>
|
||||
</b-modal>
|
||||
|
||||
<b-modal
|
||||
@@ -282,8 +304,8 @@ import PageBlock from 'corteza-webapp-compose/src/components/PageBlocks'
|
||||
import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar'
|
||||
import { compose, NoID } from '@cortezaproject/corteza-js'
|
||||
import Configurator from 'corteza-webapp-compose/src/components/PageBlocks/Configurator'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
|
||||
import MagnificationModal from 'corteza-webapp-compose/src/components/Public/Page/Block/Modal'
|
||||
import { fetchID } from 'corteza-webapp-compose/src/lib/block'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
@@ -320,9 +342,14 @@ export default {
|
||||
return {
|
||||
processing: false,
|
||||
|
||||
editor: undefined,
|
||||
page: undefined,
|
||||
layout: undefined,
|
||||
layouts: [],
|
||||
|
||||
blocks: [],
|
||||
|
||||
editor: undefined,
|
||||
|
||||
unsavedBlocks: new Set(),
|
||||
}
|
||||
},
|
||||
@@ -411,6 +438,9 @@ export default {
|
||||
return this.disableClone ? this.$t('tooltip.saveAsCopy') : ''
|
||||
},
|
||||
|
||||
selectableExistingBlocks () {
|
||||
return this.page.blocks.filter(({ blockID }) => !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
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="py-3">
|
||||
<div
|
||||
v-if="page"
|
||||
class="py-3"
|
||||
>
|
||||
<portal to="topbar-title">
|
||||
{{ $t('edit.edit') }}
|
||||
</portal>
|
||||
@@ -17,7 +20,7 @@
|
||||
>
|
||||
{{ $t('label.pageBuilder') }}
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'cogs']"
|
||||
:icon="['far', 'edit']"
|
||||
class="ml-2"
|
||||
/>
|
||||
</b-button>
|
||||
@@ -44,142 +47,297 @@
|
||||
</portal>
|
||||
|
||||
<b-container fluid="xl">
|
||||
<b-row no-gutters>
|
||||
<b-col>
|
||||
<b-card
|
||||
no-body
|
||||
class="shadow-sm"
|
||||
<b-card
|
||||
no-body
|
||||
class="shadow-sm"
|
||||
>
|
||||
<b-form-row
|
||||
v-if="page"
|
||||
class="px-4 py-3"
|
||||
>
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form
|
||||
class="px-4 py-3"
|
||||
<b-form-group
|
||||
:label="`${$t('newPlaceholder')} *`"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-row>
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
<input
|
||||
id="id"
|
||||
v-model="page.pageID"
|
||||
required
|
||||
type="hidden"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="page.title"
|
||||
data-test-id="input-title"
|
||||
required
|
||||
:state="titleState"
|
||||
class="mb-2"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('label.handle')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="page.handle"
|
||||
data-test-id="input-handle"
|
||||
:state="handleState"
|
||||
class="mb-2"
|
||||
:placeholder="$t('block.general.placeholder.handle')"
|
||||
/>
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('block.general.invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('label.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="page.description"
|
||||
data-test-id="input-description"
|
||||
:placeholder="$t('edit.pageDescription')"
|
||||
rows="4"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('icon.page') }}
|
||||
<b-button
|
||||
:title="$t('icon.configure')"
|
||||
variant="outline-light"
|
||||
class="d-flex align-items-center px-1 text-primary border-0 ml-1"
|
||||
@click="openIconModal"
|
||||
>
|
||||
<b-form-group
|
||||
:label="`${$t('newPlaceholder')} *`"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<input
|
||||
id="id"
|
||||
v-model="page.pageID"
|
||||
required
|
||||
type="hidden"
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'cog']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<img
|
||||
v-if="icon.src"
|
||||
:src="pageIcon"
|
||||
width="auto"
|
||||
height="50"
|
||||
>
|
||||
|
||||
<span v-else>
|
||||
{{ $t('icon.noIcon') }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('edit.otherOptions')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-if="!isRecordPage"
|
||||
v-model="page.visible"
|
||||
data-test-id="checkbox-page-visibility"
|
||||
>
|
||||
{{ $t('edit.visible') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="page.config.navItem.expanded"
|
||||
data-test-id="checkbox-show-sub-pages-in-sidebar"
|
||||
>
|
||||
{{ $t('showSubPages') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<hr>
|
||||
<b-form-group
|
||||
label="Layouts"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-table-simple
|
||||
responsive="lg"
|
||||
borderless
|
||||
small
|
||||
>
|
||||
<b-thead>
|
||||
<tr>
|
||||
<th />
|
||||
|
||||
<th
|
||||
class="text-primary"
|
||||
style="width: 45%; min-width: 200px;"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="page.title"
|
||||
data-test-id="input-title"
|
||||
required
|
||||
:state="titleState"
|
||||
class="mb-2"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
Title *
|
||||
</th>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('label.handle')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="page.handle"
|
||||
data-test-id="input-handle"
|
||||
:state="handleState"
|
||||
class="mb-2"
|
||||
:placeholder="$t('block.general.placeholder.handle')"
|
||||
/>
|
||||
<b-form-invalid-feedback :state="handleState">
|
||||
{{ $t('block.general.invalid-handle-characters') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<th
|
||||
class="text-primary"
|
||||
style="width: 45%; min-width: 200px;"
|
||||
>
|
||||
Handle
|
||||
</th>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('label.description')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-textarea
|
||||
v-model="page.description"
|
||||
data-test-id="input-description"
|
||||
:placeholder="$t('edit.pageDescription')"
|
||||
rows="4"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
<th style="width: 80px;" />
|
||||
</tr>
|
||||
</b-thead>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
<draggable
|
||||
v-model="layouts"
|
||||
handle=".handle"
|
||||
tag="b-tbody"
|
||||
>
|
||||
<b-form-group
|
||||
label-class="d-flex align-items-center text-primary"
|
||||
<tr
|
||||
v-for="(layout, index) in layouts"
|
||||
:key="index"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('icon.page') }}
|
||||
<b-button
|
||||
:title="$t('icon.configure')"
|
||||
variant="outline-light"
|
||||
class="d-flex align-items-center px-1 text-primary border-0 ml-1"
|
||||
@click="openIconModal"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'cog']"
|
||||
<b-td class="handle text-center align-middle pr-2">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bars']"
|
||||
class="grab m-0 text-light p-0"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td
|
||||
class="align-middle"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="layout.meta.title"
|
||||
:state="layoutTitleState(layout.meta.title)"
|
||||
@input="layout.meta.updated = true"
|
||||
/>
|
||||
</b-td>
|
||||
|
||||
<b-td
|
||||
class="align-middle"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="layout.handle"
|
||||
:state="layoutHandleState(layout.handle)"
|
||||
@input="layout.meta.updated = true"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<img
|
||||
v-if="icon.src"
|
||||
:src="pageIcon"
|
||||
width="auto"
|
||||
height="50"
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="light"
|
||||
class="d-flex align-items-center px-3"
|
||||
@click="configureLayout(index)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'wrench']"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="d-flex align-items-center"
|
||||
:to="{ name: 'admin.pages.builder', query: { layoutID: layout.pageLayoutID} }"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'edit']"
|
||||
/>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-td>
|
||||
|
||||
<td
|
||||
class="text-center align-middle"
|
||||
>
|
||||
|
||||
<span v-else>
|
||||
{{ $t('icon.noIcon') }}
|
||||
</span>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
|
||||
<b-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('edit.otherOptions')"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-if="!isRecordPage"
|
||||
v-model="page.visible"
|
||||
data-test-id="checkbox-page-visibility"
|
||||
>
|
||||
{{ $t('edit.visible') }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="page.config.navItem.expanded"
|
||||
data-test-id="checkbox-show-sub-pages-in-sidebar"
|
||||
>
|
||||
{{ $t('showSubPages') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<c-input-confirm
|
||||
:title="$t('tabs.tooltip.delete')"
|
||||
class="ml-2"
|
||||
@confirmed="deleteLayout(index)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</b-table-simple>
|
||||
<b-button
|
||||
variant="primary"
|
||||
@click="addLayout"
|
||||
>
|
||||
Add layout
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
</b-card>
|
||||
</b-container>
|
||||
|
||||
<b-modal
|
||||
v-if="layoutEditor.layout"
|
||||
:visible="!!layoutEditor.layout"
|
||||
title="Configure layout"
|
||||
:ok-title="$t('general:label.saveAndClose')"
|
||||
ok-variant="primary"
|
||||
cancel-variant="link"
|
||||
size="lg"
|
||||
@ok="updateLayout()"
|
||||
@cancel="layoutEditor.layout = undefined"
|
||||
>
|
||||
<b-form-group
|
||||
label="Condition"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-button variant="dark">
|
||||
ƒ
|
||||
</b-button>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
v-model="layoutEditor.layout.config.visibility.expression"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
label="Roles"
|
||||
label-class="text-primary"
|
||||
>
|
||||
<vue-select
|
||||
v-model="currentLayoutRoles"
|
||||
:options="roles.options"
|
||||
:loading="roles.processing"
|
||||
:get-option-label="role => role.name"
|
||||
:reduce="role => role.roleID"
|
||||
:selectable="role => !currentLayoutRoles.includes(role.roleID)"
|
||||
append-to-body
|
||||
multiple
|
||||
class="bg-white"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
|
||||
<b-modal
|
||||
v-model="showIconModal"
|
||||
:title="$t('icon.configure')"
|
||||
@@ -319,8 +477,10 @@ import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToo
|
||||
import PageTranslator from 'corteza-webapp-compose/src/components/Admin/Page/PageTranslator'
|
||||
import pages from 'corteza-webapp-compose/src/mixins/pages'
|
||||
import Uploader from 'corteza-webapp-compose/src/components/Public/Page/Attachment/Uploader'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { compose, NoID } from '@cortezaproject/corteza-js'
|
||||
import { handle } from '@cortezaproject/corteza-vue'
|
||||
import { VueSelect } from 'vue-select'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
@@ -333,6 +493,8 @@ export default {
|
||||
EditorToolbar,
|
||||
PageTranslator,
|
||||
Uploader,
|
||||
Draggable,
|
||||
VueSelect,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
@@ -361,6 +523,19 @@ export default {
|
||||
linkUrl: '',
|
||||
|
||||
processing: false,
|
||||
layouts: [],
|
||||
|
||||
layoutEditor: {
|
||||
index: undefined,
|
||||
layout: undefined,
|
||||
},
|
||||
|
||||
deletedLayouts: new Set(),
|
||||
|
||||
roles: {
|
||||
processing: false,
|
||||
options: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -390,11 +565,11 @@ export default {
|
||||
},
|
||||
|
||||
hasChildren () {
|
||||
return this.pages.some(({ selfID }) => 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)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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')))
|
||||
},
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
style="margin-left:2px;"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'cogs']"
|
||||
:icon="['far', 'edit']"
|
||||
/>
|
||||
</b-button>
|
||||
<namespace-translator
|
||||
|
||||
@@ -157,6 +157,9 @@ export default {
|
||||
this.$store.dispatch('page/load', p)
|
||||
.catch(this.errHandler),
|
||||
|
||||
this.$store.dispatch('pageLayout/load', p)
|
||||
.catch(this.errHandler),
|
||||
|
||||
]).catch(this.errHandler).then(() => {
|
||||
this.loaded = true
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
>
|
||||
{{ $t('general:label.pageBuilder') }}
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'cogs']"
|
||||
:icon="['far', 'edit']"
|
||||
class="ml-2"
|
||||
/>
|
||||
</b-button>
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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<KV> {
|
||||
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<KV> {
|
||||
const {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -27,16 +27,18 @@ interface PageBlockMeta {
|
||||
|
||||
export type PageBlockInput = PageBlock | Partial<PageBlock>
|
||||
|
||||
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 = {}
|
||||
|
||||
|
||||
106
lib/js/src/compose/types/page-layout.ts
Normal file
106
lib/js/src/compose/types/page-layout.ts
Normal file
@@ -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<PageLayout>
|
||||
|
||||
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<PageBlock>)[] = [];
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Omit<Page, 'children' | 'blocks' | 'createdAt' | 'updatedAt' | 'deletedAt'>> {
|
||||
interface PartialPage extends Partial<Omit<Page, 'children' | 'meta' | 'blocks' |'createdAt' | 'updatedAt' | 'deletedAt'>> {
|
||||
children?: Array<PartialPage>;
|
||||
|
||||
blocks?: (Partial<PageBlock>)[];
|
||||
blocks?: PageBlock[];
|
||||
|
||||
meta?: object;
|
||||
|
||||
createdAt?: string|number|Date;
|
||||
updatedAt?: string|number|Date;
|
||||
@@ -14,15 +16,6 @@ interface PartialPage extends Partial<Omit<Page, 'children' | 'blocks' | 'create
|
||||
}
|
||||
|
||||
interface PageConfig {
|
||||
buttons: {
|
||||
submit: Button;
|
||||
delete: Button;
|
||||
new: Button;
|
||||
edit: Button;
|
||||
clone: Button;
|
||||
back: Button;
|
||||
};
|
||||
attachments: [];
|
||||
navItem: {
|
||||
icon: {
|
||||
type: string;
|
||||
@@ -47,20 +40,11 @@ export class Page {
|
||||
|
||||
public visible = false;
|
||||
|
||||
public children?: Array<Page>
|
||||
public children?: Page[];
|
||||
|
||||
public blocks: (InstanceType<typeof PageBlock>)[] = [];
|
||||
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<PageBlock>(i.blocks, 'kind') && AreObjectsOf<PageBlock>(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')) {
|
||||
|
||||
Reference in New Issue
Block a user