3
0

Add tab block to compose

This commit is contained in:
Emmy Leke
2022-12-16 19:01:49 +01:00
committed by Jože Fortun
parent 23f0e4a1fa
commit c6f88e2679
22 changed files with 849 additions and 68 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -15,4 +15,5 @@ export const Report = require('./Report.png')
export const Progress = require('./Progress.png')
export const Nylas = require('./Nylas.jpg')
export const Geometry = require('./Geometry.png')
export const Tabs = require('./Tabs.png')
export const Navigation = require('./Navigation.png')

View File

@@ -8,7 +8,7 @@
<b-list-group-item
v-for="(type) in types"
:key="type.label"
:disabled="!recordPage && type.recordPageOnly"
:disabled="isOptionDisabled(type)"
button
@click="$emit('select', type.block)"
@mouseover="current = type.image"
@@ -56,6 +56,11 @@ export default {
type: Boolean,
default: false,
},
disabledKinds: {
type: Array,
default: () => [],
},
},
data () {
@@ -149,6 +154,11 @@ export default {
block: new compose.PageBlockGeometry(),
image: images.Geometry,
},
{
label: this.$t('tabs.label'),
block: new compose.PageBlockTab(),
image: images.Tabs,
},
{
label: this.$t('navigation.label'),
block: new compose.PageBlockNavigation(),
@@ -157,5 +167,11 @@ export default {
],
}
},
methods: {
isOptionDisabled (type) {
return (!this.recordPage && type.recordPageOnly) || this.disabledKinds.includes(type.block.kind)
},
},
}
</script>

View File

@@ -1,5 +1,7 @@
<template>
<div class="d-flex flex-column align-items-center justify-content-center h-100 position-relative">
<div
class="d-flex flex-column align-items-center justify-content-center position-relative h-100"
>
<div
v-if="processing"
class="d-flex flex-column align-items-center justify-content-center flex-fill"

View File

@@ -19,27 +19,31 @@
:use-css-transforms="false"
@layout-updated="handleLayoutUpdate"
>
<grid-item
<template
v-for="(item, index) in gridCollection"
:key="item.i"
ref="items"
class="grid-item"
:class="{
'h-100': isStretchable,
}"
style="touch-action: none;"
v-bind="{ ...item }"
@moved="onBlockUpdated(index)"
@resized="onBlockUpdated(index)"
>
<slot
:block="blocks[item.i]"
:index="index"
:block-index="item.i"
:bounding-rect="boundingRects[index]"
v-on="$listeners"
/>
</grid-item>
<grid-item
v-if="blocks[item.i].meta.tabbed !== true"
:key="item.i"
ref="items"
class="grid-item"
:class="{
'h-100': isStretchable,
}"
style="touch-action: none;"
v-bind="{ ...item }"
@moved="onBlockUpdated(index)"
@resized="onBlockUpdated(index)"
>
<slot
:block="blocks[item.i]"
:index="index"
:block-index="item.i"
:bounding-rect="boundingRects[index]"
v-on="$listeners"
/>
</grid-item>
</template>
</grid-layout>
</div>
<div

View File

@@ -148,6 +148,19 @@
/>
</b-form-group>
</b-col>
<b-col
v-if="showTabOption"
cols="12"
>
<b-form-checkbox
v-model="block.meta.tabbed"
switch
class="mb-2"
>
{{ $t('general.tabbed.label') }}
</b-form-checkbox>
</b-col>
</b-row>
</b-tab>
@@ -215,6 +228,10 @@ export default {
{ value: 'fullscreen', text: this.$t('general.magnifyOptions.fullscreen') },
]
},
showTabOption () {
return this.block.kind !== 'Tabs' && this.block.meta !== undefined
},
},
methods: {

View File

@@ -0,0 +1,87 @@
<template>
<wrap
v-bind="$props"
:scrollable-body="false"
v-on="$listeners"
>
<div
v-if="!options.tabs.length"
class="d-flex h-100 align-items-center justify-content-center"
>
<p class="mb-0">
{{ $t('tabs.noTabsBase') }}
</p>
</div>
<b-tabs
v-else
v-model="activeTab"
nav-wrapper-class="bg-white white border-bottom"
card
content-class="flex-fill card overflow-hidden"
v-bind="{
align: block.options.style.alignment,
fill: block.options.style.fillJustify === 'fill',
justified: block.options.style.fillJustify === 'justified',
pills: block.options.style.appearance === 'pills',
tabs: block.options.style.appearance === 'tabs',
small: block.options.style.appearance === 'small',
vertical: block.options.style.verticalHorizontal === 'vertical',
}"
class="d-flex flex-column h-100"
>
<b-tab
v-for="(tab, index) in tabbedBlocks"
:key="index"
:title="tab.title || tab.block.title || `Block-${tab.block.kind}`"
class="h-100 "
title-item-class="text-truncate"
title-link-class="text-truncate"
no-body
>
<page-block-tab
lazy
v-bind="{ ...$attrs, ...$props, page, block: tab.block, blockIndex: index }"
:record="record"
:module="module"
/>
</b-tab>
</b-tabs>
</wrap>
</template>
<script>
import base from './base'
import { compose } from '@cortezaproject/corteza-js'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs.js'
export default {
i18nOptions: {
namespaces: 'block',
},
name: 'TabBase',
components: {
PageBlockTab: () => import('corteza-webapp-compose/src/components/PageBlocks'),
},
extends: base,
data () {
return {
activeTab: 0,
}
},
computed: {
tabbedBlocks () {
return this.block.options.tabs.map(({ blockID, title }) => {
return {
block: compose.PageBlockMaker(this.page.blocks.find(b => fetchID(b) === blockID)),
title,
}
})
},
},
}
</script>

View File

@@ -0,0 +1,337 @@
<template>
<b-tab title="Tabs">
<div>
<h5 class="text-primary">
{{ $t('tabs.displayTitle') }}
</h5>
<b-row
class="mb-3 mt-3 ml-0 mr-0 justify-content-between"
no-gutters
>
<b-form-group label="Appearance">
<b-form-radio-group
v-model="block.options.style.appearance"
buttons
button-variant="outline-primary"
size="sm"
:options="style.appearance"
/>
</b-form-group>
<b-form-group label="Alignment">
<b-form-radio-group
v-model="block.options.style.alignment"
buttons
button-variant="outline-primary"
size="sm"
:options="style.alignment"
/>
</b-form-group>
<b-form-group label="Fill or Justify">
<b-form-radio-group
v-model="block.options.style.fillJustify"
buttons
button-variant="outline-primary"
size="sm"
:options="style.fillJustify"
/>
</b-form-group>
</b-row>
</div>
<div
class="d-flex"
>
<h5
class="font-weight-light m-0 p-0 text-primary"
>
{{ $t('tabs.title') }}
</h5>
<b-button
variant="link"
size="md"
:title="shouldDisableAdd ? $t('tabs.tooltip.tabCondition') : $t('tabs.tooltip.addTab')"
:disabled="shouldDisableAdd"
class="p-0 ml-3 text-decoration-none"
@click="addTab"
>
{{ $t('tabs.addTab') }}
</b-button>
</div>
<b-table-simple
v-if="block.options.tabs.length"
borderless
small
>
<b-thead>
<tr>
<th />
<th
class="text-primary"
>
{{ $t('tabs.table.columns.title.label') }}
</th>
<th
class="text-primary"
>
{{ $t('tabs.table.columns.block.label') }}
</th>
<th />
</tr>
</b-thead>
<draggable
v-model="block.options.tabs"
handle=".handle"
tag="b-tbody"
>
<tr
v-for="(tab, index) in block.options.tabs"
:key="index"
>
<b-td class="handle align-middle">
<font-awesome-icon
:icon="['fas', 'bars']"
class="grab m-0 text-light p-0"
/>
</b-td>
<b-td
class="align-middle"
style="width: 50%"
>
<b-form-input
v-model="tab.title"
:title="$t('tabs.tooltip.title')"
:disabled="!tab.blockID"
:placeholder="$t('tabs.form.title')"
/>
</b-td>
<b-td
class="align-middle"
style="width: 50%"
>
<div
class="d-flex"
>
<vue-select
v-model="tab.blockID"
:title="$t('tabs.tooltip.selectBlock')"
:options="options"
:placeholder="$t('tabs.form.placeholder')"
:selectable="option => isSelectable(option)"
class="block-selector bg-white m-0"
append-to-body
style="min-width: 95%;"
:reduce="option => option.value"
>
<template #list-footer>
<b-button
id="CreateBlockSelectorTab"
variant="link"
size="sm"
:title="$t('tabs.tooltip.newBlock')"
class="text-decoration-none"
block
@click="showBlockSelector(index)"
>
{{ $t('tabs.addTab') }}
</b-button>
</template>
</vue-select>
<b-button
id="popover-edit"
size="sm"
:disabled="!tab.blockID"
:title="$t('tabs.tooltip.edit')"
variant="light"
@click="editBlock(tab.blockID)"
>
<font-awesome-icon
:icon="['far', 'edit']"
/>
</b-button>
</div>
</b-td>
<td
class="text-right align-middle pr-2"
style="min-width: 100px;"
>
<c-input-confirm
:title="$t('tabs.tooltip.delete')"
@confirmed="deleteTab(index)"
/>
</td>
</tr>
</draggable>
</b-table-simple>
<div
v-else
class="text-center pt-5 pb-5"
>
<p>
{{ $t('tabs.noTabs') }}
</p>
</div>
<b-modal
id="createBlockSelectorTab"
size="lg"
scrollable
hide-footer
:title="$t('tabs.newBlockModal')"
>
<new-block-selector
:record-page="!!module"
:disable-kind="['Tabs']"
@select="addBlock"
/>
</b-modal>
</b-tab>
</template>
<script>
import base from './base'
import draggable from 'vuedraggable'
import { VueSelect } from 'vue-select'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs.js'
export default {
i18nOptions: {
namespaces: 'block',
},
name: 'TabConfigurator',
components: {
draggable,
VueSelect,
// Importing like this because configurator is recursive
NewBlockSelector: () => import('corteza-webapp-compose/src/components/Admin/Page/Builder/Selector'),
},
extends: base,
data () {
return {
activeIndex: null,
style: {
appearance: [
{ text: this.$t('tabs.style.appearance.tabs'), value: 'tabs', disabled: false },
{ text: this.$t('tabs.style.appearance.pills'), value: 'pills', disabled: false },
{ text: this.$t('tabs.style.appearance.small'), value: 'small', disabled: false },
],
alignment: [
{ text: this.$t('tabs.style.alignment.left'), value: 'left', disabled: false },
{ text: this.$t('tabs.style.alignment.center'), value: 'center', disabled: false },
{ text: this.$t('tabs.style.alignment.right'), value: 'right', disabled: false },
],
fillJustify: [
{ text: this.$t('tabs.style.fillJustify.fill'), value: 'fill', disabled: false },
{ text: this.$t('tabs.style.fillJustify.justified'), value: 'justified', disabled: false },
{ text: this.$t('tabs.style.fillJustify.none'), value: 'none', disabled: false },
],
},
untabbedBlock: [],
}
},
computed: {
options () {
return this.page.blocks.filter(b => b.kind !== 'Tabs').map((b, i) => {
// block title is going to look ugly till you save the page. Inevitable.
return { value: fetchID(b), label: b.title || `Block-${b.kind}` }
})
},
shouldDisableAdd () {
return this.page.blocks.find(b => fetchID(b) === fetchID(this.block)) === undefined
},
},
created () {
this.$root.$on('builder-createRequestFulfilled', this.createRequestFulfilled)
},
destroyed () {
this.$root.$off('builder-createRequestFulfilled', this.createRequestFulfilled)
},
methods: {
createRequestFulfilled ({ tab }) {
if (tab) {
this.updateTab(tab, this.activeIndex)
}
},
addTab () {
this.block.options.tabs.push({
blockID: null,
title: undefined,
})
},
isSelectable (option) {
return !this.block.options.tabs.some(t => t.blockID === option.value)
},
showBlockSelector (index) {
this.$bvModal.show('createBlockSelectorTab')
this.activeIndex = index
},
editBlock (blockID = undefined) {
this.$root.$emit('tab-editRequest', blockID)
},
addBlock (block) {
this.$bvModal.hide('createBlockSelectorTab')
block.meta.tabbed = true
this.$root.$emit('tab-createRequest', block)
},
updateTab (tab, index) {
this.block.options.tabs.splice(index, 1, tab)
},
deleteTab (tabIndex) {
this.block.options.tabs.splice(tabIndex, 1)
},
},
}
</script>
<style lang="scss">
.block-selector {
.vs__selected-options {
flex-wrap: nowrap;
}
.vs__selected {
max-width: 200px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.vs__dropdown-menu .vs__dropdown-option {
text-overflow: ellipsis;
overflow: hidden !important;
}
</style>

View File

@@ -38,6 +38,8 @@ import GeometryBase from './GeometryBase'
import GeometryConfigurator from './GeometryConfigurator/index'
import NavigationConfigurator from './Navigation/Configurator'
import NavigationBase from './Navigation/Base'
import TabsBase from './TabsBase'
import TabsConfigurator from './TabsConfigurator'
/**
* List of all known page block components
@@ -79,6 +81,8 @@ const Registry = {
NylasConfigurator,
GeometryBase,
GeometryConfigurator,
TabsBase,
TabsConfigurator,
NavigationConfigurator,
NavigationBase,
}

View File

@@ -0,0 +1,8 @@
import { NoID } from '@cortezaproject/corteza-js'
/**
* If block has no ID, it is a new block and we need to use tempID
* to find it in the list of blocks.
*/
export function fetchID (block) {
return block.blockID === NoID ? block.meta.tempID : block.blockID
}

View File

@@ -149,7 +149,7 @@
:visible="showCreator"
body-class="p-0 border-top-0"
header-class="p-3 pb-0 border-bottom-0"
@ok="updateBlocks"
@ok="$event => updateBlocks()"
@hide="editor = undefined"
>
<configurator
@@ -164,15 +164,11 @@
<b-modal
:title="$t('changeBlock')"
:ok-title="$t('label.saveAndClose')"
ok-variant="primary"
:cancel-title="$t('label.cancel')"
cancel-variant="link"
size="xl"
:visible="showEditor"
body-class="p-0 border-top-0"
footer-class="d-flex justify-content-between"
header-class="p-3 pb-0 border-bottom-0"
@ok="updateBlocks"
@hide="editor = undefined"
>
<configurator
@@ -184,6 +180,39 @@
:block-index="editor.index"
:record="record"
/>
<template #modal-footer="{ cancel }">
<c-input-confirm
size="md"
size-confirm="md"
variant="danger"
:title="$t('label.delete')"
:borderless="false"
@confirmed="deleteBlock(editor.index)"
>
{{ $t('label.delete') }}
</c-input-confirm>
<div>
<b-button
variant="link"
size="md"
:title="$t('label.cancel')"
class="text-decoration-none"
@click="cancel()"
>
{{ $t('label.cancel') }}
</b-button>
<b-button
variant="primary"
size="md"
:title="$t('label.saveAndClose')"
@click="updateBlocks()"
>
{{ $t('label.saveAndClose') }}
</b-button>
</div>
</template>
</b-modal>
<portal to="admin-toolbar">
@@ -246,6 +275,7 @@ 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.js'
export default {
i18nOptions: {
@@ -284,6 +314,7 @@ export default {
blocks: [],
board: null,
unsavedBlocks: new Set(),
id: null,
}
},
@@ -370,6 +401,7 @@ export default {
cloneTooltip () {
return this.disableClone ? this.$t('tooltip.saveAsCopy') : ''
},
},
watch: {
@@ -392,6 +424,12 @@ export default {
},
},
created () {
this.$root.$on('tab-editRequest', this.fulfilEditRequest)
this.$root.$on('tab-createRequest', this.fulfilCreateRequest)
this.$root.$on('tabChange', this.untabBlock)
},
mounted () {
window.addEventListener('paste', this.pasteBlock)
},
@@ -406,6 +444,8 @@ export default {
destroyed () {
window.removeEventListener('paste', this.pasteBlock)
this.$root.$off('tab-editRequest', this.fulfilEditRequest)
this.$root.$off('tab-createRequest', this.fulfilCreateRequest)
},
methods: {
@@ -418,6 +458,39 @@ export default {
loadPages: 'page/load',
}),
fulfilEditRequest (blockID) {
// this ensures whatever changes in tabs is not lost before we lose its configurator
// because we are reusing that modal component
this.updateBlocks()
this.blocks.find((block, i) => fetchID(block) === blockID && this.editBlock(i))
},
fulfilCreateRequest (block) {
this.updateBlocks(block)
},
untabBlock (block) {
const where = this.tabLocation(block)
if (!where.length) return
where.forEach(({ block, index }) => {
const { tabs } = block.options
tabs.splice(index, 1)
})
},
tabLocation (tabbedBlock) {
const where = []
this.blocks.forEach((block, i) => {
if (block.kind !== 'Tabs') return
const { tabs } = block.options
const index = tabs.findIndex(({ blockID }) => blockID === fetchID(tabbedBlock))
where.push({ block, index })
})
return where
},
addBlock (block, index = undefined) {
this.$bvModal.hide('createBlockSelector')
this.editor = { index, block: compose.PageBlockMaker(block) }
@@ -428,8 +501,44 @@ export default {
},
deleteBlock (index) {
/**
* If the block is tabbed, we need to remove it from the tabs when it is deleted.
* We find where it is tabbed then remove it from its tabs.
* We don't bother checking if it is actually tabbed because you cannot delete a
* manually tabed block that is not added to a Tabs block.
*/
if (this.blocks[index].meta.tabbed) {
const whereIsItTabbed = this.blocks.filter((block) => block.kind === 'Tabs' && block.options.tabs.some(({ blockID }) => blockID === fetchID(this.blocks[index])))
whereIsItTabbed.forEach((block) => {
block.options.tabs = block.options.tabs.filter(({ blockID }) => blockID !== fetchID(this.blocks[index]))
})
}
/**
* First we get all the tabs that are tabbed in other Tabs blocks.
* The reduce eliminates duplicates as we only need reps of what is tabbed
* We check what needs to be freed by comparing allTabs to the tabs of the block
* being deleted.
*/
if (this.blocks[index].kind === 'Tabs') {
const allTabs = this.blocks.filter((block) => block.kind === 'Tabs' && fetchID(block) !== fetchID(this.blocks[index]))
.map(({ options }) => options.tabs).flat().reduce((unique, o) => {
if (!unique.some(tab => tab.blockID === o.blockID)) {
unique.push(o)
}
return unique
}, []).map(({ blockID }) => blockID)
const tobefreed = this.blocks[index].options.tabs.filter(({ blockID }) => !allTabs.includes(blockID))
tobefreed.forEach(({ blockID }) => {
this.blocks.find((b) => fetchID(b) === blockID).meta.tabbed = false
})
}
this.blocks.splice(index, 1)
this.page.blocks = this.blocks
if (this.editor) this.editor = undefined
this.unsavedBlocks.add(index)
},
@@ -441,22 +550,71 @@ export default {
this.unsavedBlocks.add(index)
},
updateBlocks () {
const block = compose.PageBlockMaker(this.editor.block)
generateUID () {
return Math.random().toString(36).substring(2) + (new Date()).getTime().toString(36)
},
updateBlocks (block = this.editor.block) {
block = compose.PageBlockMaker(block)
this.page.blocks = this.blocks
/**
* Check if an existing block has been updated or a new block has been added
*/
if (this.editor.index !== undefined) {
this.page.blocks.splice(this.editor.index, 1, block)
this.unsavedBlocks.add(this.editor.index)
const oldBlock = this.blocks[this.editor.index]
if (oldBlock.meta.tabbed === true && this.editor.block.meta.tabbed === false) {
this.untabBlock(this.editor.block)
}
if (this.editor.block.kind !== block.kind) {
this.page.blocks.push(block)
const tab = {
blockID: fetchID(block),
title: block.title,
}
this.$root.$emit('builder-createRequestFulfilled', { tab })
} else {
this.page.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.editor = undefined
if (block.kind === 'Tabs') {
block.options.tabs = block.options.tabs.filter(t => t.blockID !== null)
block.options.tabs.forEach((tab) => {
this.page.blocks.find(b => fetchID(b) === tab.blockID).meta.tabbed = true
})
}
if (this.editor.block.kind === block.kind) {
this.editor = undefined
}
},
cloneBlock (index) {
this.appendBlock({ ...this.blocks[index].clone() }, this.$t('notification:page.cloneSuccess'))
},
appendBlock (block, msg) {
if (this.blocks.length) {
// ensuring we append the block to the end of the page
// eslint-disable-next-line
const maxY = this.blocks.map((block) => block.xywh[1]).reduce((acc, val) => {
return acc > val ? acc : val
}, 0)
block.xywh = [0, maxY + 2, 3, 3]
}
this.editor = { index: undefined, block: compose.PageBlockMaker(block) }
this.updateBlocks()
if (!this.editor) {
msg && this.toastSuccess(msg)
return true
} else {
msg && this.toastErrorHandler(this.$t('notification:page.duplicateFailed'))
return false
}
},
async handleSave ({ closeOnSuccess = false, previewOnSuccess = false } = {}) {
@@ -493,6 +651,32 @@ export default {
const mergedPage = new compose.Page({ namespaceID, ...page, blocks: this.blocks })
this.updatePage(mergedPage).then((page) => {
// get the Tabs Block that still has tabs with tempIDs
const tabsWithTempIDs = this.page.blocks.filter(b => b.kind === 'Tabs').filter(b => b.options.tabs.some(t => t.blockID.startsWith('tempID-')))
if (!tabsWithTempIDs.length) return page
page.blocks.forEach(b => {
if (b.kind !== 'Tabs') return
tabsWithTempIDs.find(t => t.meta.tempID === b.meta.tempID).options.tabs.forEach((t, j) => {
if (!t.blockID.startsWith('tempID-')) return false
// find a block with the same tempID that should be updated by now and get its blockID
const updatedBlock = page.blocks.find(block => block.meta.tempID === t.blockID)
if (!updatedBlock) return false
const tab = {
// fetchID gets the blockID using the found block
blockID: fetchID(updatedBlock),
title: updatedBlock.title,
}
b.options.tabs.splice(j, 1, tab)
})
})
return this.updatePage(page)
}).then((page) => {
this.unsavedBlocks.clear()
this.toastSuccess(this.$t('notification:page.saved'))
if (closeOnSuccess) {
@@ -549,12 +733,8 @@ export default {
return true
},
cloneBlock (index) {
this.appendBlock({ ...this.blocks[index] }, this.$t('notification:page.cloneSuccess'))
},
async copyBlock (index) {
const parsedBlock = JSON.stringify(this.blocks[index])
const parsedBlock = JSON.stringify(this.blocks[index].clone())
navigator.clipboard.writeText(parsedBlock).then(() => {
this.toastSuccess(this.$t('notification:page.copySuccess'))
this.toastInfo(this.$t('notification:page.blockWaiting'))
@@ -585,27 +765,6 @@ export default {
}
},
appendBlock (block, msg) {
if (this.blocks.length) {
// ensuring we append the block to the end of the page
const maxY = this.blocks.map((block) => block.xywh[1]).reduce((acc, val) => {
return acc > val ? acc : val
}, 0)
block.xywh = [0, maxY + 2, 3, 3]
}
// Doing this to avoid blockID duplicates when saved
block.blockID = NoID
this.editor = { index: undefined, block: compose.PageBlockMaker(block) }
this.updateBlocks()
if (!this.editor) {
this.toastSuccess(msg)
} else {
this.toastErrorHandler(this.$t('notification:page.duplicateFailed'))
}
},
// Trigger browser dialog on page leave to prevent unsaved changes
checkUnsavedBlocks (next) {
next(!this.unsavedBlocks.size || window.confirm(this.$t('build.unsavedChanges')))

View File

@@ -0,0 +1,4 @@
export function generateUID (): string {
let uid = Math.random().toString(36).substring(2) + (new Date()).getTime().toString(36)
return `tempID-${uid}`
}

View File

@@ -1,5 +1,6 @@
import { merge } from 'lodash'
import { Apply } from '../../../cast'
import { generateUID } from '../../helpers/idgen'
interface PageBlockStyleVariants {
[_: string]: string;
@@ -19,6 +20,11 @@ interface PageBlockStyle {
border?: PageBlockStyleBorder;
}
interface PageBlockMeta {
tabbed?: boolean;
tempID?: string;
}
export type PageBlockInput = PageBlock | Partial<PageBlock>
const defaultXYWH = [0, 2000, 3, 3]
@@ -34,6 +40,11 @@ export class PageBlock {
public options = {}
public meta: PageBlockMeta = {
tabbed: false,
tempID: undefined,
}
public style: PageBlockStyle = {
variants: {
headerText: 'dark',
@@ -48,6 +59,7 @@ export class PageBlock {
constructor (i?: PageBlockInput) {
this.apply(i)
this.setTempID()
}
apply (i?: PageBlockInput): void {
@@ -75,12 +87,29 @@ export class PageBlock {
if (i.style) {
this.style = merge({}, this.style, i.style)
}
if (i.meta) {
this.meta = merge({}, this.meta, i.meta)
}
}
// Returns Page Block configuration errors
validate (): Array<string> {
return []
}
setTempID (): void {
this.meta.tempID = this.meta.tempID || generateUID()
}
clone(): PageBlockInput {
const clone = new PageBlock()
clone.kind = this.kind
clone.title = this.title
clone.style = merge({}, this.style)
clone.options = merge({}, this.options)
return clone
}
}
export const Registry = new Map<string, typeof PageBlock>()

View File

@@ -35,6 +35,7 @@ class CalendarOptions {
public feeds: Array<Feed> = []
public header: Partial<CalendarOptionsHeader> = {}
public locale = 'en-gb'
public tabbed = false
public refreshRate = 0
public showRefresh = false
public magnifyOption = ''
@@ -75,6 +76,7 @@ export class PageBlockCalendar extends PageBlock {
)
this.options.locale = o.locale || 'en-gb'
this.options.tabbed = o.tabbed || false
}
/**

View File

@@ -17,14 +17,14 @@ interface Options {
}
const defaults: Readonly<Options> = Object.freeze({
chartID: '',
chartID: NoID,
refreshRate: 0,
showRefresh: false,
magnifyOption: '',
drillDown: {
enabled: false,
blockID: ''
}
blockID: '',
},
})
export class PageBlockChart extends PageBlock {

View File

@@ -15,10 +15,11 @@ export { PageBlockComment } from './comment'
export { PageBlockReport } from './report'
export { PageBlockProgress } from './progress'
export { PageBlockNylas } from './nylas'
export { PageBlockGeometry } from './geometry'
export { PageBlockNavigation } from './navigation'
export { PageBlockTab } from './tabs'
export { PageBlockGeometry } from './geometry'
export function PageBlockMaker<T extends PageBlock> (i: { kind: string }): T {
export function PageBlockMaker<T extends PageBlock>(i: { kind: string }): T {
const PageBlockTemp = Registry.get(i.kind)
if (PageBlockTemp === undefined) {
throw new Error(`unknown block kind '${i.kind}'`)

View File

@@ -103,7 +103,7 @@ export class PageBlockProgress extends PageBlock {
}
if (o.display) {
this.options.display = { ...this.options.display, ...o.display }
this.options.display = o.display
}
}

View File

@@ -16,6 +16,7 @@ interface Options {
// referenced fields (records, users) we want to expand
expRefFields: string[];
refreshRate: number;
showRefresh: boolean;
magnifyOption: string;

View File

@@ -0,0 +1,53 @@
import { PageBlock, PageBlockInput, Registry } from './base'
const kind = 'Tabs'
interface Style {
appearance: string;
alignment: string;
fillJustify: string;
}
interface Tab {
blockID: string;
title: string;
}
interface Options {
style: Style;
tabs: Tab[];
}
const defaults: Readonly<Options> = Object.freeze({
style: {
appearance: 'tabs',
alignment: 'left',
fillJustify: 'none',
},
tabs: [],
})
export class PageBlockTab extends PageBlock {
readonly kind = kind
options: Options = { ...defaults }
constructor (i?: PageBlockInput) {
super(i)
this.applyOptions(i?.options as Partial<Options>)
}
applyOptions (o?: Partial<Options>): void {
if (!o) return
if (o.tabs) {
this.options.tabs = o.tabs
}
if (o.style) {
this.options.style = o.style
}
}
}
Registry.set(kind, PageBlockTab)

View File

@@ -123,6 +123,8 @@ general:
modal: Modal
fullscreen: Fullscreen
wrap: Display block as a card
tabbed:
label: Tabbed
refresh:
label: Refresh
auto: Auto refresh
@@ -694,3 +696,54 @@ geometry:
lockBounds: Lock bounds
topLeft: Bounds top left
lowerRight: Bounds lower right
tabs:
alertTitle: Set a title for your block
title: Tabs
addTab: + Add
selectBlock: Choose a block
noTabs: No tabs have been made
noTabsBase: No tabs have been made yet
displayTitle: Display Options
preview: Live example
newBlockModal: Add new block
form:
title: Set a title for your tab
placeholder: Tab a block
style:
appearance:
tabs: Tabs
pills: Pills
small: Small
alignment:
left: Left
center: Center
right: Right
fillJustify:
fill: Fill
justified: Justify
none: None
verticalHorizontal:
vertical: Vertical
horizontal: Horizontal
tabPosition:
top: Top
bottom: Bottom
table:
columns:
title:
label: Title
block:
label: Block
tooltip:
edit: Edit block
changeTab: Change tab
addTab: Add tab
cancel: Switch to view mode
newBlock: Add new block
delete: Delete tab
selectBlock: Select a block to tab
move: Move tab
title: Tab title
tabCondition: You can't make tabs until you add this block
label: Tabs

View File

@@ -33,6 +33,7 @@ label:
pageBuilder: Page builder
permissions: Permissions
saveAndClose: Save and close
delete: Delete Block
loading: Loading
moduleEdit: Edit module
navigation:

View File

@@ -3,11 +3,12 @@ package types
import (
"database/sql/driver"
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/sql"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/spf13/cast"
@@ -58,6 +59,7 @@ type (
Style PageBlockStyle `json:"style,omitempty"`
Kind string `json:"kind"`
XYWH [4]int `json:"xywh"` // x,y,w,h
Meta map[string]any `json:"meta,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us