Adjust tab block style and behaviour

This commit is contained in:
Jože Fortun
2023-03-10 15:16:13 +01:00
parent c6f88e2679
commit a4308e8d88
20 changed files with 419 additions and 289 deletions
@@ -1,7 +1,5 @@
<template>
<div
class="d-flex flex-column align-items-center justify-content-center position-relative h-100"
>
<div class="d-flex flex-column align-items-center justify-content-center h-100 position-relative">
<div
v-if="processing"
class="d-flex flex-column align-items-center justify-content-center flex-fill"
@@ -13,9 +13,9 @@
:row-height="50"
:is-resizable="!!editable"
:is-draggable="!!editable"
:responsive="!editable"
:cols="columnNumber"
:margin="[0, 0]"
:responsive="!editable"
:use-css-transforms="false"
@layout-updated="handleLayoutUpdate"
>
@@ -23,7 +23,7 @@
v-for="(item, index) in gridCollection"
>
<grid-item
v-if="blocks[item.i].meta.tabbed !== true"
v-if="!blocks[item.i].meta.hidden"
:key="item.i"
ref="items"
class="grid-item"
@@ -161,7 +161,10 @@ export default {
deep: true,
handler (blocks) {
if (blocks.length === 0) this.$emit('change', [])
this.grid = blocks.map(({ xywh: [x, y, w, h] }, i) => ({ i, x, y, w, h }))
this.grid = 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 }
})
},
},
},
@@ -75,7 +75,7 @@
</div>
<div
ref="cc"
:ref="`cc-${blockIndex}`"
class="h-100"
>
<div
@@ -87,7 +87,7 @@
<full-calendar
v-show="show && !processing"
ref="fc"
:ref="`fc-${blockIndex}`"
:key="key"
:height="getHeight()"
:events="events"
@@ -259,7 +259,9 @@ export default {
// Proxy to the FC API
api () {
return this.$refs.fc.getApi()
if (this.$refs[`fc-${this.blockIndex}`]) {
return this.$refs[`fc-${this.blockIndex}`].getApi()
}
},
/**
@@ -342,8 +344,8 @@ export default {
},
getHeight () {
if (this.$refs.cc) {
return this.$refs.cc.clientHeight
if (this.$refs[`cc-${this.blockIndex}`]) {
return this.$refs[`cc-${this.blockIndex}`].clientHeight
}
return 'auto'
},
@@ -7,7 +7,6 @@
<vue-select
v-model="block.options.chartID"
:options="charts"
:clearable="true"
:placeholder="$t('chart.pick')"
:reduce="option => option.chartID"
label="name"
@@ -6,19 +6,10 @@
card
lazy
>
<template #tabs-end>
<page-translator
v-if="page"
:page="page"
:block.sync="block"
:disabled="isNew"
button-variant="link"
/>
</template>
<b-tab
data-test-id="general-tab"
active
title-item-class="order-first"
:title="$t('general.label.general')"
>
<b-row>
@@ -77,6 +68,7 @@
<b-col
cols="12"
sm="6"
class="mb-2"
>
<b-form-group
:label="$t('general.headerStyle')"
@@ -103,6 +95,14 @@
>
{{ $t('general.border.show') }}
</b-form-checkbox>
<b-form-checkbox
v-if="block.kind !== 'Tabs'"
v-model="block.meta.hidden"
switch
>
{{ $t('general.hidden.label') }}
</b-form-checkbox>
</b-col>
<b-col
@@ -148,19 +148,6 @@
/>
</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>
@@ -170,6 +157,16 @@
class="mh-tab overflow-auto"
v-on="$listeners"
/>
<template #tabs-end>
<page-translator
v-if="page"
:page="page"
:block.sync="block"
:disabled="isNew"
button-variant="link"
/>
</template>
</b-tabs>
</template>
<script>
@@ -228,10 +225,6 @@ export default {
{ value: 'fullscreen', text: this.$t('general.magnifyOptions.fullscreen') },
]
},
showTabOption () {
return this.block.kind !== 'Tabs' && this.block.meta !== undefined
},
},
methods: {
@@ -9,41 +9,54 @@
class="d-flex h-100 align-items-center justify-content-center"
>
<p class="mb-0">
{{ $t('tabs.noTabsBase') }}
{{ $t('tabs.noTabs') }}
</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"
nav-class="bg-white"
:nav-wrapper-class="navWrapperClass"
:content-class="contentClass"
v-bind="{
align: block.options.style.alignment,
fill: block.options.style.fillJustify === 'fill',
justified: block.options.style.fillJustify === 'justified',
justified: block.options.style.fillJustify === 'justify',
pills: block.options.style.appearance === 'pills',
tabs: block.options.style.appearance === 'tabs',
small: block.options.style.appearance === 'small',
vertical: block.options.style.verticalHorizontal === 'vertical',
vertical: block.options.style.orientation === 'vertical',
end: block.options.style.position === 'end'
}"
class="d-flex flex-column h-100"
class="h-100"
:class="{ 'd-flex flex-column': block.options.style.orientation !== 'vertical' }"
>
<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"
:key="`${getTabTitle(tab, index)}-${index}`"
:title="getTabTitle(tab, index)"
class="h-100"
:title-item-class="getTitleItemClass(index)"
:title-link-class="getTitleItemClass(index)"
no-body
:lazy="isTabLazy(tab)"
>
<page-block-tab
lazy
v-bind="{ ...$attrs, ...$props, page, block: tab.block, blockIndex: index }"
v-if="tab.block"
v-bind="{ ...$attrs, ...$props, page, block: tab.block, blockIndex: index, boundingRect: { xywh: block.xywh} }"
:record="record"
:module="module"
/>
<div
v-else
class="d-flex h-100 align-items-center justify-content-center"
>
<p class="mb-0">
{{ $t('tabs.noBlock') }}
</p>
</div>
</b-tab>
</b-tabs>
</wrap>
@@ -52,7 +65,7 @@
<script>
import base from './base'
import { compose } from '@cortezaproject/corteza-js'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs.js'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
export default {
i18nOptions: {
@@ -67,21 +80,52 @@ export default {
extends: base,
data () {
return {
activeTab: 0,
}
},
computed: {
tabbedBlocks () {
return this.block.options.tabs.map(({ blockID, title }) => {
const block = this.page.blocks.find(b => fetchID(b) === blockID)
return {
block: compose.PageBlockMaker(this.page.blocks.find(b => fetchID(b) === blockID)),
block: block ? compose.PageBlockMaker(block) : undefined,
title,
}
})
},
contentClass () {
return `card overflow-hidden mh-100 ${this.block.options.style.orientation === 'vertical' ? 'd-block' : 'flex-fill'}`
},
navWrapperClass () {
const { orientation, position } = this.block.options.style
let border = 'border-bottom'
let style = 'bg-white mh-100'
if (orientation === 'vertical') {
border = position === 'end' ? 'border-left' : 'border-right'
style = `${style} overflow-auto`
} else if (position === 'end') {
border = 'border-top'
}
return `${border} ${style}`
},
},
methods: {
getTitleItemClass (index) {
const { fillJustify, alignment } = this.block.options.style
return `order-${index} text-truncate text-${alignment} ${fillJustify !== 'none' ? 'flex-fill' : ''}`
},
getTabTitle ({ title, block = {} }, tabIndex) {
const { title: blockTitle, kind } = block
return title || blockTitle || kind || `${this.$t('tabs.tab')} ${tabIndex + 1}`
},
isTabLazy ({ block = {} }) {
const { kind } = block
return ['Calendar', 'Metric', 'Geometry'].includes(kind)
},
},
}
</script>
@@ -1,59 +1,98 @@
<template>
<b-tab title="Tabs">
<div>
<h5 class="text-primary">
{{ $t('tabs.displayTitle') }}
</h5>
<b-tab :title="$t('tabs.label')">
<h5>
{{ $t('tabs.displayTitle') }}
</h5>
<b-row
class="mb-3 mt-3 ml-0 mr-0 justify-content-between"
no-gutters
<b-row
class="text-primary"
no-gutters
>
<b-col
cols="12"
md="4"
>
<b-form-group label="Appearance">
<b-form-group :label="$t('tabs.style.appearance.label')">
<b-form-radio-group
v-model="block.options.style.appearance"
:options="style.appearance"
buttons
button-variant="outline-primary"
size="sm"
:options="style.appearance"
/>
</b-form-group>
</b-col>
<b-form-group label="Alignment">
<b-col
cols="12"
md="4"
>
<b-form-group :label="$t('tabs.style.orientation.label')">
<b-form-radio-group
v-model="block.options.style.orientation"
:options="style.orientation"
buttons
button-variant="outline-primary"
size="sm"
/>
</b-form-group>
</b-col>
<b-col
cols="12"
md="4"
>
<b-form-group :label="$t('tabs.style.position.label')">
<b-form-radio-group
v-model="block.options.style.position"
:options="style.position"
buttons
button-variant="outline-primary"
size="sm"
/>
</b-form-group>
</b-col>
<b-col
cols="12"
md="4"
>
<b-form-group :label="$t('tabs.style.alignment.label')">
<b-form-radio-group
v-model="block.options.style.alignment"
:options="style.alignment"
buttons
button-variant="outline-primary"
size="sm"
:options="style.alignment"
/>
</b-form-group>
<b-form-group label="Fill or Justify">
</b-col>
<b-col
cols="12"
md="4"
>
<b-form-group :label="$t('tabs.style.fillJustify.label')">
<b-form-radio-group
v-model="block.options.style.fillJustify"
:options="style.fillJustify"
buttons
button-variant="outline-primary"
size="sm"
:options="style.fillJustify"
/>
</b-form-group>
</b-row>
</div>
</b-col>
</b-row>
<div
class="d-flex"
class="d-flex align-items-center mb-2"
>
<h5
class="font-weight-light m-0 p-0 text-primary"
>
<h5 class="m-0">
{{ $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"
>
@@ -63,6 +102,7 @@
<b-table-simple
v-if="block.options.tabs.length"
responsive="lg"
borderless
small
>
@@ -95,7 +135,7 @@
v-for="(tab, index) in block.options.tabs"
:key="index"
>
<b-td class="handle align-middle">
<b-td class="handle align-middle pr-2">
<font-awesome-icon
:icon="['fas', 'bars']"
class="grab m-0 text-light p-0"
@@ -104,67 +144,66 @@
<b-td
class="align-middle"
style="width: 50%"
style="width: 50%; min-width: 200px;"
>
<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%"
style="width: 50%; min-width: 200px;"
>
<div
class="d-flex"
>
<b-input-group class="d-flex flex-nowrap w-100">
<vue-select
v-model="tab.blockID"
:title="$t('tabs.tooltip.selectBlock')"
:options="options"
:placeholder="$t('tabs.form.placeholder')"
:options="blockOptions"
:placeholder="$t('tabs.placeholder.block')"
:get-option-label="getBlockLabel"
: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>
:calculate-position="calculatePosition"
append-to-body
class="block-selector bg-white"
/>
<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-input-group-append>
<b-button
v-if="tab.blockID"
id="popover-edit"
size="sm"
variant="light"
:title="blockEditDisabled ? $t('tabs.tooltip.editDisabled') : $t('tabs.tooltip.edit')"
:disabled="blockEditDisabled"
class="d-flex align-items-center justify-content-center"
style="width: 40px;"
@click="editBlock(tab.blockID)"
>
<font-awesome-icon
:icon="['far', 'edit']"
/>
</b-button>
<b-button
v-else
size="sm"
variant="light"
:title="$t('tabs.tooltip.addBlock')"
class="d-flex align-items-center justify-content-center"
style="width: 40px;"
@click="showBlockSelector(index)"
>
<font-awesome-icon
:icon="['fas', 'plus']"
/>
</b-button>
</b-input-group-append>
</b-input-group>
</b-td>
<td
class="text-right align-middle pr-2"
style="min-width: 100px;"
class="text-center align-middle"
style="min-width: 80px;"
>
<c-input-confirm
:title="$t('tabs.tooltip.delete')"
@@ -177,7 +216,7 @@
<div
v-else
class="text-center pt-5 pb-5"
class="text-center my-4"
>
<p>
{{ $t('tabs.noTabs') }}
@@ -193,7 +232,7 @@
>
<new-block-selector
:record-page="!!module"
:disable-kind="['Tabs']"
:disabled-kinds="['Tabs']"
@select="addBlock"
/>
</b-modal>
@@ -204,7 +243,8 @@
import base from './base'
import draggable from 'vuedraggable'
import { VueSelect } from 'vue-select'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs.js'
import calculatePosition from 'corteza-webapp-compose/src/mixins/vue-select-position'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
export default {
i18nOptions: {
@@ -222,42 +262,51 @@ export default {
extends: base,
mixins: [
calculatePosition,
],
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 },
{ text: this.$t('tabs.style.appearance.tabs'), value: 'tabs' },
{ text: this.$t('tabs.style.appearance.pills'), value: 'pills' },
{ text: this.$t('tabs.style.appearance.small'), value: 'small' },
],
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 },
{ text: this.$t('tabs.style.alignment.left'), value: 'left' },
{ text: this.$t('tabs.style.alignment.center'), value: 'center' },
{ text: this.$t('tabs.style.alignment.right'), value: 'right' },
],
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 },
{ text: this.$t('tabs.style.fillJustify.fill'), value: 'fill' },
{ text: this.$t('tabs.style.fillJustify.justify'), value: 'justify' },
{ text: this.$t('tabs.style.fillJustify.none'), value: 'none' },
],
orientation: [
{ text: this.$t('tabs.style.orientation.horizontal'), value: 'horizontal' },
{ text: this.$t('tabs.style.orientation.vertical'), value: 'vertical' },
],
position: [
{ text: this.$t('tabs.style.position.start'), value: 'start' },
{ text: this.$t('tabs.style.position.end'), value: 'end' },
],
},
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}` }
})
blockOptions () {
return this.page.blocks.filter(b => b.kind !== 'Tabs').map(b => ({ ...b, value: fetchID(b) }))
},
shouldDisableAdd () {
blockEditDisabled () {
return this.page.blocks.find(b => fetchID(b) === fetchID(this.block)) === undefined
},
},
@@ -271,10 +320,8 @@ export default {
},
methods: {
createRequestFulfilled ({ tab }) {
if (tab) {
this.updateTab(tab, this.activeIndex)
}
createRequestFulfilled (tab) {
this.updateTab(tab, this.activeIndex)
},
addTab () {
@@ -299,7 +346,7 @@ export default {
addBlock (block) {
this.$bvModal.hide('createBlockSelectorTab')
block.meta.tabbed = true
block.meta.hidden = true
this.$root.$emit('tab-createRequest', block)
},
@@ -310,22 +357,43 @@ export default {
deleteTab (tabIndex) {
this.block.options.tabs.splice(tabIndex, 1)
},
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;
&: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 {
flex-wrap: nowrap;
// do not allow growing
width: 0;
}
.vs__selected {
max-width: 200px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
display: block;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
}
}
@@ -333,5 +401,4 @@ export default {
text-overflow: ellipsis;
overflow: hidden !important;
}
</style>
@@ -2,7 +2,7 @@
<div class="h-100">
<b-card
no-body
class="h-100 shadow"
class="h-100 shadow overflow-hidden"
:class="blockClass"
>
<b-card-header
@@ -1,7 +1,7 @@
<template>
<div class="h-100">
<div
class="card bg-transparent h-100"
class="card bg-transparent h-100 overflow-hidden"
:class="blockClass"
>
<div
@@ -14,7 +14,7 @@ export default {
scrollableBody: {
type: Boolean,
required: false,
default: () => true,
default: true,
},
},
+45 -4
View File
@@ -1,4 +1,5 @@
import { NoID } from '@cortezaproject/corteza-js'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
export default {
methods: {
@@ -46,21 +47,61 @@ export default {
newPageTitle = `${this.$t('copyOf')}${this.page.title} (${countDuplicates})`
}
// Change tabbed blockID to use tempID's since they are persisted on save
const blocks = page.blocks.map(block => {
if (block.kind !== 'Tabs') return block
const { tabs = [] } = block.options
block.options.tabs = tabs.map(b => {
const { tempID } = (page.blocks.find(({ blockID }) => blockID === b.blockID) || {}).meta || {}
b.blockID = tempID
return b
})
return block
})
page = {
...page,
blocks,
pageID: NoID,
title: newPageTitle,
handle: '',
}
return this.createPage({ namespaceID, ...page })
})
.then(({ pageID }) => {
return this.createPage({ namespaceID, ...page }).then(this.updateTabbedBlockIDs)
}).then(({ pageID }) => {
this.$router.push({ name: this.$route.name, params: { pageID } })
})
.catch(this.toastErrorHandler(this.$t('notification:page.cloneFailed')))
},
},
async updateTabbedBlockIDs (page) {
// get the Tabs Block that still has tabs with tempIDs
page.blocks.filter(({ kind }) => kind === 'Tabs')
.filter(({ options = {} }) => options.tabs.some(({ blockID }) => (blockID || '').startsWith('tempID-')))
.forEach(b => {
if (b.kind !== 'Tabs') return
b.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)
},
},
}
@@ -108,6 +108,7 @@
:icon="['far', 'copy']"
/>
</b-button>
<c-input-confirm
class="ml-1"
size="md"
@@ -149,7 +150,7 @@
:visible="showCreator"
body-class="p-0 border-top-0"
header-class="p-3 pb-0 border-bottom-0"
@ok="$event => updateBlocks()"
@ok="updateBlocks()"
@hide="editor = undefined"
>
<configurator
@@ -195,7 +196,6 @@
<div>
<b-button
variant="link"
size="md"
:title="$t('label.cancel')"
class="text-decoration-none"
@click="cancel()"
@@ -205,7 +205,6 @@
<b-button
variant="primary"
size="md"
:title="$t('label.saveAndClose')"
@click="updateBlocks()"
>
@@ -221,6 +220,7 @@
:hide-delete="hideDelete"
:hide-save="!page.canUpdatePage"
:disable-clone="disableClone"
:disable-save="processing"
:clone-tooltip="cloneTooltip"
@save="handleSave()"
@delete="handleDeletePage"
@@ -275,7 +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'
import { fetchID } from 'corteza-webapp-compose/src/lib/tabs'
export default {
i18nOptions: {
@@ -309,12 +309,12 @@ export default {
data () {
return {
processing: false,
editor: undefined,
page: undefined,
blocks: [],
board: null,
unsavedBlocks: new Set(),
id: null,
}
},
@@ -501,27 +501,17 @@ 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) => {
// If the deleted block is hidden, we need to remove it from the related tabs blocks if it is tabbed.
if (this.blocks[index].meta.hidden) {
this.blocks.forEach((block) => {
if (block.kind !== 'Tabs' || !block.options.tabs.some(({ blockID }) => blockID === fetchID(this.blocks[index]))) return
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.
*/
// Changes meta.hidden property to false, for all blocks that were tabbed only in the deleted block
if (this.blocks[index].kind === 'Tabs') {
const allTabs = this.blocks.filter((block) => block.kind === 'Tabs' && fetchID(block) !== fetchID(this.blocks[index]))
const tabbedBlocks = this.blocks.filter((block) => block.kind === 'Tabs' && fetchID(block) !== fetchID(this.blocks[index]))
.map(({ options }) => options.tabs).flat().reduce((unique, o) => {
if (!unique.some(tab => tab.blockID === o.blockID)) {
unique.push(o)
@@ -529,9 +519,12 @@ export default {
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[index].options.tabs.forEach(({ blockID }) => {
if (tabbedBlocks.includes(blockID)) return
const index = this.blocks.findIndex((b) => fetchID(b) === blockID)
if (index === -1) return
this.blocks[index].meta.hidden = false
this.calculateNewBlockPosition(this.blocks[index])
})
}
@@ -550,10 +543,6 @@ export default {
this.unsavedBlocks.add(index)
},
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
@@ -561,17 +550,17 @@ export default {
if (this.editor.index !== undefined) {
const oldBlock = this.blocks[this.editor.index]
if (oldBlock.meta.tabbed === true && this.editor.block.meta.tabbed === false) {
if (oldBlock.meta.hidden === true && this.editor.block.meta.hidden === false) {
this.untabBlock(this.editor.block)
this.calculateNewBlockPosition(block)
}
if (this.editor.block.kind !== block.kind) {
this.page.blocks.push(block)
const tab = {
this.$root.$emit('builder-createRequestFulfilled', {
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)
@@ -582,9 +571,9 @@ export default {
}
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 (!tab.blockID) return
this.page.blocks.find(b => fetchID(b) === tab.blockID).meta.hidden = true
})
}
@@ -594,20 +583,15 @@ export default {
},
cloneBlock (index) {
this.appendBlock({ ...this.blocks[index].clone() }, this.$t('notification:page.cloneSuccess'))
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.calculateNewBlockPosition(block)
this.editor = { index: undefined, block: compose.PageBlockMaker(block) }
this.updateBlocks()
if (!this.editor) {
msg && this.toastSuccess(msg)
return true
@@ -617,6 +601,17 @@ export default {
}
},
calculateNewBlockPosition (block) {
if (this.blocks.length) {
// ensuring we append the block to the end of the page
// eslint-disable-next-line
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]
}
},
async handleSave ({ closeOnSuccess = false, previewOnSuccess = false } = {}) {
const { namespaceID } = this.namespace
@@ -645,48 +640,27 @@ export default {
return
}
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 })
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) {
this.$router.push({ name: 'admin.pages' })
} else if (previewOnSuccess) {
this.$router.push({ name: 'page', params: { pageID: this.pageID } })
}
this.page = new compose.Page(page)
}).catch(this.toastErrorHandler(this.$t('notification:page.saveFailed')))
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')))
},
validateModuleFieldSelection (module, page) {
@@ -734,8 +708,20 @@ export default {
},
async copyBlock (index) {
const parsedBlock = JSON.stringify(this.blocks[index].clone())
navigator.clipboard.writeText(parsedBlock).then(() => {
const block = JSON.stringify(this.blocks[index].clone())
// Change tabbed blockID to use tempID's since they are persisted on save
if (block.kind === 'Tabs') {
const { tabs = [] } = block.options
block.options.tabs = tabs.map(b => {
const { tempID } = (this.blocks.find(({ blockID }) => blockID === b.blockID) || {}).meta || {}
b.blockID = tempID
return b
})
}
navigator.clipboard.writeText(block).then(() => {
this.toastSuccess(this.$t('notification:page.copySuccess'))
this.toastInfo(this.$t('notification:page.blockWaiting'))
this.$refs.pageBuilder.focus()
+6 -10
View File
@@ -1,6 +1,7 @@
import { merge } from 'lodash'
import { Apply } from '../../../cast'
import { Apply, NoID } from '../../../cast'
import { generateUID } from '../../helpers/idgen'
import { PageBlockMaker } from './'
interface PageBlockStyleVariants {
[_: string]: string;
@@ -21,7 +22,7 @@ interface PageBlockStyle {
}
interface PageBlockMeta {
tabbed?: boolean;
hidden?: boolean;
tempID?: string;
}
@@ -41,7 +42,7 @@ export class PageBlock {
public options = {}
public meta: PageBlockMeta = {
tabbed: false,
hidden: false,
tempID: undefined,
}
@@ -102,13 +103,8 @@ export class PageBlock {
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
clone (): PageBlockInput {
return PageBlockMaker({ ...this, blockID: NoID, meta: { tempID: '' } })
}
}
@@ -35,7 +35,6 @@ 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 = ''
@@ -76,7 +75,6 @@ export class PageBlockCalendar extends PageBlock {
)
this.options.locale = o.locale || 'en-gb'
this.options.tabbed = o.tabbed || false
}
/**
+3 -3
View File
@@ -17,14 +17,14 @@ interface Options {
}
const defaults: Readonly<Options> = Object.freeze({
chartID: NoID,
chartID: '',
refreshRate: 0,
showRefresh: false,
magnifyOption: '',
drillDown: {
enabled: false,
blockID: '',
},
blockID: ''
}
})
export class PageBlockChart extends PageBlock {
+1 -1
View File
@@ -19,7 +19,7 @@ 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}'`)
@@ -103,7 +103,7 @@ export class PageBlockProgress extends PageBlock {
}
if (o.display) {
this.options.display = o.display
this.options.display = { ...this.options.display, ...o.display }
}
}
@@ -16,7 +16,6 @@ interface Options {
// referenced fields (records, users) we want to expand
expRefFields: string[];
refreshRate: number;
showRefresh: boolean;
magnifyOption: string;
+7 -3
View File
@@ -6,6 +6,8 @@ interface Style {
appearance: string;
alignment: string;
fillJustify: string;
orientation: string;
position: string;
}
interface Tab {
@@ -21,8 +23,10 @@ interface Options {
const defaults: Readonly<Options> = Object.freeze({
style: {
appearance: 'tabs',
alignment: 'left',
fillJustify: 'none',
alignment: 'center',
fillJustify: 'justify',
orientation: 'horizontal',
position: 'start',
},
tabs: [],
})
@@ -45,7 +49,7 @@ export class PageBlockTab extends PageBlock {
}
if (o.style) {
this.options.style = o.style
this.options.style = { ...this.options.style, ...o.style }
}
}
}
+19 -19
View File
@@ -123,8 +123,8 @@ general:
modal: Modal
fullscreen: Fullscreen
wrap: Display block as a card
tabbed:
label: Tabbed
hidden:
label: Hidden
refresh:
label: Refresh
auto: Auto refresh
@@ -701,34 +701,39 @@ tabs:
alertTitle: Set a title for your block
title: Tabs
addTab: + Add
tab: Tab
selectBlock: Choose a block
noTabs: No tabs have been made
noTabsBase: No tabs have been made yet
noTabs: No tabs configured
noBlock: No block configured
displayTitle: Display Options
preview: Live example
newBlockModal: Add new block
form:
title: Set a title for your tab
placeholder: Tab a block
placeholder:
block: Select a block to tab
style:
appearance:
label: Appearance
tabs: Tabs
pills: Pills
small: Small
alignment:
label: Alignment
left: Left
center: Center
right: Right
fillJustify:
label: Fill or Justify
fill: Fill
justified: Justify
justify: Justify
none: None
verticalHorizontal:
orientation:
label: Orientation
vertical: Vertical
horizontal: Horizontal
tabPosition:
top: Top
bottom: Bottom
position:
label: Position
start: Start
end: End
table:
columns:
title:
@@ -737,13 +742,8 @@ tabs:
label: Block
tooltip:
edit: Edit block
changeTab: Change tab
editDisabled: Editing tabbed blocks is disabled until you save this block
addTab: Add tab
cancel: Switch to view mode
newBlock: Add new block
addBlock: 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