3
0

Add navigational page block

This commit is contained in:
Kelani Tolulope
2022-12-09 11:47:10 +01:00
parent d5ce490906
commit 41f1283e9d
16 changed files with 972 additions and 2 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -15,3 +15,4 @@ 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 Navigation = require('./Navigation.png')
@@ -149,6 +149,11 @@ export default {
block: new compose.PageBlockGeometry(),
image: images.Geometry,
},
{
label: this.$t('navigation.label'),
block: new compose.PageBlockNavigation(),
image: images.Navigation,
},
],
}
},
@@ -0,0 +1,158 @@
<template>
<wrap
:scrollable-body="false"
v-bind="$props"
v-on="$listeners"
>
<div class="h-100 w-100 card overflow-hidden bg-transparent">
<b-nav
:tabs="options.display.appearance === 'tabs'"
:pills="options.display.appearance === 'pills'"
:small="options.display.appearance === 'small'"
:fill="options.display.fillJustify === 'fill'"
:justify="options.display.fillJustify === 'justify'"
:align="options.display.alignment"
class="border-0 h-100"
>
<b-nav-item
v-for="(navItem, index) in options.navigationItems"
:key="`navItem-${index}`"
:disabled="!navItem.options.enabled"
:style="{ order: index, color: navItem.options.textColor, background: navItem.options.backgroundColor, justifyContent: options.display.alignment }"
:link-attrs="{ style: `color: ${navItem.options.textColor}` }"
:target="selectTargetOption(navItem.options.item.target)"
:href="redirectForNavItem(navItem)"
class="d-flex align-items-center"
>
<template v-if="navItem.type === 'dropdown' || isComposeDropdownPage(navItem)">
<b-button
:id="`dropdown-popover-${index}-${block.blockID}`"
class="text-decoration-none"
variant="link"
:style="{ color: navItem.options.textColor, background: navItem.options.backgroundColor }"
>
{{ displayDropdownText(navItem) }}
<span class="ml-1">
<font-awesome-icon
:icon="['fas', 'chevron-down']"
size="sm"
/>
</span>
</b-button>
<b-popover
ref="dropdown-popover"
:target="`dropdown-popover-${index}-${block.blockID}`"
:placement="navItem.options.item.align"
delay="0"
boundary="window"
triggers="click blur"
>
<template
v-if="navItem.type === 'dropdown'"
>
<div
v-for="(dropdown, dIndex) in navItem.options.item.dropdown.items"
:key="`dropdown-${dIndex}`"
>
<a
class="dropdown-item"
:href="dropdown.url"
:disabled="navItem.options.disabled"
:target="selectTargetOption(dropdown.target)"
:style="{ order: dIndex * 2 }"
>
{{ dropdown.label }}
</a>
<hr
v-if="dropdown.delimiter"
class="my-1"
:style="{ order: dIndex + 1 }"
>
</div>
</template>
<template v-else>
<div
v-for="(dropdown, dIndex) in getSubPages(navItem.options.item.pageID)"
:key="`dropdown-${dIndex}`"
>
<b-link
:to="{ name: 'page', params: { pageID: dropdown.pageID } }"
:target="selectTargetOption(navItem.options.item.target)"
:style="{ order: dIndex * 2 }"
:disabled="navItem.options.disabled"
class="dropdown-item"
style="white-space: normal"
>
{{ dropdown.title }}
</b-link>
</div>
</template>
</b-popover>
</template>
<template v-else>
{{ navItem.options.item.label }}
</template>
</b-nav-item>
</b-nav>
</div>
</wrap>
</template>
<script>
import { NoID } from '@cortezaproject/corteza-js'
import { mapGetters } from 'vuex'
import base from '../base'
export default {
extends: base,
computed: {
...mapGetters({
pages: 'page/set',
}),
},
methods: {
isComposeDropdownPage (navItem) {
return (navItem.type === 'compose' && navItem.options.item.displaySubPages)
},
getSubPages (selfID) {
return this.pages.filter(value => value.selfID === selfID && value.moduleID === NoID) || []
},
selectTargetOption (target) {
switch (target) {
case 'sameTab':
return '_self'
case 'newTab':
return '_blank'
}
},
displayDropdownText (navItem) {
if (navItem.type === 'dropdown') {
return navItem.options.item.dropdown.label
}
return navItem.options.item.label
},
redirectForNavItem (navItem) {
if (navItem.type === 'dropdown' || this.isComposeDropdownPage(navItem)) {
return
} else if (navItem.type === 'compose') {
const slug = this.$route.params.slug
const pageID = navItem.options.item.pageID
return `ns/${slug}/pages/${pageID}`
}
return navItem.options.item.url
},
},
}
</script>
@@ -0,0 +1,274 @@
<template>
<div>
<b-tab :title="$t('navigation.label')">
<div class="mb-3">
<h5 class="text-primary">
{{ $t("navigation.displayOptions") }}
</h5>
<b-row class="justify-content-between">
<b-col
cols="12"
sm="4"
class="mb-2 mb-sm-0"
>
<b-form-group
horizontal
:label="$t('navigation.appearance')"
>
<b-form-radio-group
v-model="options.display.appearance"
buttons
button-variant="outline-secondary"
:options="appearanceOptions"
/>
</b-form-group>
</b-col>
<b-col
sm="4"
class="mb-2 mb-sm-0"
>
<b-form-group
horizontal
:label="$t('navigation.fillJustify')"
>
<b-form-radio-group
v-model="options.display.fillJustify"
buttons
button-variant="outline-secondary"
:options="fillJustifyOptions"
/>
</b-form-group>
</b-col>
<b-col
sm="4"
class="mb-2 mb-sm-0"
>
<b-form-group
horizontal
:label="$t('navigation.alignment')"
>
<b-form-radio-group
v-model="options.display.alignment"
buttons
button-variant="outline-secondary"
:options="alignmentOptions"
/>
</b-form-group>
</b-col>
</b-row>
</div>
<div class="mb-3 mt-2">
<div class="d-flex align-items-center mb-4">
<h5 class="text-primary mb-0">
{{ $t("navigation.navigationItems") }}
</h5>
<b-button
variant="link"
class="d-flex align-items-center text-decoration-none"
@click="addNavigationItem"
>
<font-awesome-icon
:icon="['fas', 'plus']"
size="sm"
class="mr-1"
/>
{{ $t("navigation.add") }}
</b-button>
</div>
<div class="mt-3">
<draggable
v-model="block.options.navigationItems"
group="sort"
handle=".grab"
>
<div
v-for="(item, index) in block.options.navigationItems"
:key="index"
>
<hr v-if="index">
<b-table-simple
borderless
>
<thead>
<tr>
<th style="width: auto" />
<th>
{{ $t("navigation.type") }}
</th>
<th>
{{ $t("navigation.color") }}
</th>
<th>
{{ $t("navigation.background") }}
</th>
<th class="text-center">
{{ $t("navigation.enabled") }}
</th>
<th style="width: auto;min-width: 100px" />
</tr>
</thead>
<tbody>
<tr>
<td class="align-middle">
<font-awesome-icon
:icon="['fas', 'bars']"
class="grab text-grey"
/>
</td>
<td class="align-middle">
<b-form-select
v-model="item.type"
:options="navigationItemTypes"
/>
</td>
<td class="align-middle">
<b-form-input
v-model="item.options.textColor"
type="color"
debounce="300"
class="w-100"
/>
</td>
<td class="align-middle">
<b-form-input
v-model="item.options.backgroundColor"
type="color"
debounce="300"
class="w-100"
/>
</td>
<td class="align-middle text-center">
<b-form-checkbox
v-model="item.options.enabled"
switch
class="mb-0"
/>
</td>
<td class="align-middle">
<c-input-confirm
button-class="px-2"
size="md"
@confirmed="options.navigationItems.splice(index, 1)"
/>
</td>
</tr>
<component
:is="item.type"
:item="item"
:namespace="namespace"
@update="(value) => item = value"
/>
</tbody>
</b-table-simple>
</div>
</draggable>
</div>
</div>
</b-tab>
</div>
</template>
<script>
import base from '../base'
import Draggable from 'vuedraggable'
import { compose } from '@cortezaproject/corteza-js'
import Text from './NavTypes/Text.vue'
import Url from './NavTypes/Url.vue'
import Compose from './NavTypes/ComposePage.vue'
import Dropdown from './NavTypes/Dropdown.vue'
export default {
i18nOptions: {
namespaces: 'block',
},
components: {
Draggable,
TextSection: Text,
Url,
Compose,
Dropdown,
},
extends: base,
data () {
return {
appearanceOptions: [
{ value: 'tabs', text: this.$t('navigation.tabs') },
{ value: 'pills', text: this.$t('navigation.pills') },
{ value: 'small', text: this.$t('navigation.small') },
],
alignmentOptions: [
{ value: 'left', text: this.$t('navigation.left') },
{ value: 'center', text: this.$t('navigation.center') },
{ value: 'right', text: this.$t('navigation.right') },
],
fillJustifyOptions: [
{ value: 'fill', text: this.$t('navigation.fill') },
{ value: 'justify', text: this.$t('navigation.justify') },
],
backgroundColors: [
{ value: 'primary', text: this.$t('navigation.primary') },
{ value: 'secondary', text: this.$t('navigation.secondary') },
{ value: 'success', text: this.$t('navigation.success') },
{ value: 'warning', text: this.$t('navigation.warning') },
{ value: 'danger', text: this.$t('navigation.danger') },
{ value: 'info', text: this.$t('navigation.info') },
],
navigationItemTypes: [
{ value: 'url', text: this.$t('navigation.url') },
{ value: 'compose', text: this.$t('navigation.composePage') },
{ value: 'dropdown', text: this.$t('navigation.dropdown') },
{ value: 'text-section', text: this.$t('navigation.text') },
],
}
},
methods: {
addNavigationItem () {
this.block.options.navigationItems.push(
compose.PageBlockNavigation.makeNavigationItem({
type: 'compose',
options: {
backgroundColor: '#ffffff',
item: {
align: 'bottom',
target: 'sameTab',
dropdown: {
label: '',
items: [],
},
},
},
}),
)
},
},
}
</script>
<style lang="scss" scoped>
th {
width: 25%;
}
th,
td {
padding-left: 15px;
padding-right: 15px;
}
</style>
@@ -0,0 +1,115 @@
<template>
<tr>
<td />
<td>
<b-form-group :label="$t('navigation.fieldLabel')">
<b-form-input
v-model="options.item.label"
type="text"
/>
</b-form-group>
</td>
<td>
<b-form-group :label="$t('navigation.composePage')">
<vue-select
key="pageID"
v-model="options.item.pageID"
:placeholder="$t('navigation.none')"
:options="tree"
append-to-body
label="title"
:reduce="f => f.pageID"
option-value="pageID"
option-text="title"
class="bg-white"
@input="updateLabelValue"
/>
</b-form-group>
</td>
<td>
<b-form-group :label="$t('navigation.openIn')">
<b-form-select
v-model="options.item.target"
:options="targetOptions"
/>
</b-form-group>
</td>
<td
v-if="selectedPageChildren(options.item.pageID)"
cols="12"
sm="6"
class="align-middle text-center"
>
<b-form-group class="m-0">
<b-form-checkbox
v-model="options.item.displaySubPages"
switch
size="sm"
>
{{ $t('navigation.displaySubPages') }}
</b-form-checkbox>
</b-form-group>
</td>
<td />
</tr>
</template>
<script>
import base from './base'
import { VueSelect } from 'vue-select'
import { compose } from '@cortezaproject/corteza-js'
export default {
components: {
VueSelect,
},
extends: base,
props: {
namespace: {
type: compose.Namespace,
required: true,
},
},
data () {
return {
tree: [],
targetOptions: [
{ value: 'sameTab', text: this.$t('navigation.sameTab') },
{ value: 'newTab', text: this.$t('navigation.newTab') },
],
}
},
created () {
this.loadTree()
},
methods: {
selectedPageChildren (pageID) {
const tree = this.tree ? this.tree.find(t => t.pageID === pageID) : {}
return tree && tree.children ? tree.children.length > 0 : false
},
loadTree () {
const { namespaceID } = this.namespace
this.$ComposeAPI
.pageTree({ namespaceID })
.then(tree => {
this.tree = tree.map(p => new compose.Page(p))
})
.catch(this.toastErrorHandler(this.$t('notification:page.loadFailed')))
},
updateLabelValue (pageID) {
if (!this.options.item.label) {
const composePage = this.tree.find(t => t.pageID === pageID)
this.options.item.label = composePage ? composePage.title : ''
}
},
},
}
</script>
@@ -0,0 +1,148 @@
<template>
<tr>
<td />
<td
colspan="4"
class="p-0"
>
<div class="d-flex">
<th>
<b-form-group :label="$t('navigation.fieldLabel')">
<b-form-input
v-model="options.item.dropdown.label"
type="text"
/>
</b-form-group>
</th>
<th>
<b-form-group
horizontal
:label="$t('navigation.drop')"
>
<b-form-radio-group
v-model="options.item.align"
buttons
button-variant="outline-secondary"
size="sm"
:options="aligns"
/>
</b-form-group>
</th>
</div>
<div class="d-flex align-items-center mb-4 mb-3 px-3">
<h6 class="text-primary mb-0">
{{ $t("navigation.dropdownItems") }}
</h6>
<b-button
variant="link"
class="text-decoration-none"
@click="options.item.dropdown.items.push({ text: '', url: '', target: 'sameTab' })"
>
<font-awesome-icon
:icon="['fas', 'plus']"
size="sm"
class="mr-1"
/>
{{ $t("navigation.add") }}
</b-button>
</div>
<div class="px-3">
<table
v-if="options.item.dropdown.items.length > 0"
class="table table-sm table-borderless table-responsive-lg"
>
<tr>
<th>
{{ $t("navigation.text") }}
</th>
<th>
{{ $t("navigation.url") }}
</th>
<th class="text-center">
{{ $t('navigation.openIn') }}
</th>
<th class="text-center">
{{ $t("navigation.delimiter") }}
</th>
</tr>
<tr
v-for="(item, dropIndex) in options.item.dropdown.items"
:key="`drop-${dropIndex}`"
>
<th>
<b-form-group class="mb-0">
<b-form-input
v-model="item.label"
type="text"
/>
</b-form-group>
</th>
<th>
<b-form-group class="mb-0">
<b-form-input
v-model="item.url"
type="text"
/>
</b-form-group>
</th>
<th class="align-middle text-center">
<b-form-group class="mb-0">
<b-form-select
v-model="item.target"
:options="targetOptions"
/>
</b-form-group>
</th>
<th class="align-middle text-center">
<b-form-group class="mb-0">
<b-form-checkbox
v-model="item.delimiter"
switch
size="sm"
/>
</b-form-group>
</th>
<th class="align-middle text-center">
<c-input-confirm
@confirmed="options.item.dropdown.items.splice(dropIndex, 1)"
/>
</th>
</tr>
</table>
</div>
</td>
</tr>
</template>
<script>
import base from './base'
export default {
extends: base,
data () {
return {
aligns: [
{ value: 'right', text: this.$t('navigation.right') },
{ value: 'left', text: this.$t('navigation.left') },
{ value: 'bottom', text: this.$t('navigation.bottom') },
{ value: 'top', text: this.$t('navigation.top') },
],
targetOptions: [
{ value: 'sameTab', text: this.$t('navigation.sameTab') },
{ value: 'newTab', text: this.$t('navigation.newTab') },
],
}
},
}
</script>
@@ -0,0 +1,21 @@
<template>
<tr>
<td />
<td>
<b-form-group :label="$t('navigation.fieldLabel')">
<b-form-input
v-model="options.item.label"
type="text"
/>
</b-form-group>
</td>
</tr>
</template>
<script>
import base from './base'
export default {
extends: base,
}
</script>
@@ -0,0 +1,47 @@
<template>
<tr>
<td />
<td>
<b-form-group :label="$t('navigation.fieldLabel')">
<b-form-input
v-model="options.item.label"
type="text"
/>
</b-form-group>
</td>
<td>
<b-form-group :label="$t('navigation.url')">
<b-form-input
v-model="options.item.url"
type="text"
/>
</b-form-group>
</td>
<td>
<b-form-group :label="$t('navigation.openIn')">
<b-form-select
v-model="options.item.target"
:options="openInType"
/>
</b-form-group>
</td>
</tr>
</template>
<script>
import base from './base'
export default {
extends: base,
data () {
return {
value: {},
openInType: [
{ value: 'sameTab', text: this.$t('navigation.sameTab') },
{ value: 'newTab', text: this.$t('navigation.newTab') },
],
}
},
}
</script>
@@ -0,0 +1,25 @@
<script>
export default {
i18nOptions: {
namespaces: 'block',
},
props: {
item: {
type: Object,
required: true,
},
},
computed: {
options: {
get () {
return this.item.options
},
set (options) {
this.item.options = options
},
},
},
}
</script>
@@ -75,7 +75,6 @@ export default {
get () {
return this.block.options
},
set (options) {
this.block.options = options
},
@@ -36,6 +36,8 @@ import NylasBase from './Nylas/NylasBase'
import NylasConfigurator from './Nylas/NylasConfigurator'
import GeometryBase from './GeometryBase'
import GeometryConfigurator from './GeometryConfigurator/index'
import NavigationConfigurator from './Navigation/Configurator'
import NavigationBase from './Navigation/Base'
/**
* List of all known page block components
@@ -77,6 +79,8 @@ const Registry = {
NylasConfigurator,
GeometryBase,
GeometryConfigurator,
NavigationConfigurator,
NavigationBase,
}
const defaultMode = 'Base'
+1 -1
View File
@@ -16,7 +16,7 @@ export { PageBlockReport } from './report'
export { PageBlockProgress } from './progress'
export { PageBlockNylas } from './nylas'
export { PageBlockGeometry } from './geometry'
export { PageBlockNavigation } from './navigation'
export function PageBlockMaker<T extends PageBlock> (i: { kind: string }): T {
const PageBlockTemp = Registry.get(i.kind)
@@ -0,0 +1,51 @@
import { PageBlock, PageBlockInput, Registry } from '../base'
import NavigationItem, { NavigationItemInput } from './navigation-item'
const kind = 'Navigation'
interface DisplayOptions {
appearance: string;
alignment: string;
fillJustify: string;
}
interface Options {
display: DisplayOptions;
navigationItems: NavigationItem[];
}
const defaults: Readonly<Options> = Object.freeze({
display: {
appearance: 'tabs',
alignment: 'center',
fillJustify: 'justify',
},
navigationItems: [],
})
export class PageBlockNavigation 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
this.options.navigationItems = (o.navigationItems || []).map(f => new NavigationItem(f))
if (o.display) {
this.options.display = o.display
}
}
static makeNavigationItem (item?: NavigationItemInput): NavigationItem {
return new NavigationItem(item)
}
}
Registry.set(kind, PageBlockNavigation)
@@ -0,0 +1,72 @@
import { Apply } from '../../../../cast'
interface DropdownItem {
label: string;
url: string;
delimiter: boolean;
target: string;
}
interface Dropdown {
label: string;
items: DropdownItem[]
}
interface ItemOptions {
label: string;
url: string;
target: string;
delimiter: boolean;
pageID: string;
displaySubPages: boolean;
dropdown: Dropdown;
align: string;
}
interface NavigationItemOptions {
enabled: boolean;
textColor: string;
backgroundColor: string;
item: ItemOptions;
}
export type NavigationItemInput = Partial<NavigationItem> | NavigationItem
const defOptions = {
enabled: true,
textColor: '#0B344E',
backgroundColor: '',
item: {
label: '',
url: '',
target: '',
delimiter: false,
pageID: '',
displaySubPages: false,
align: 'bottom',
dropdown: {
label: "",
items: []
},
},
}
export default class NavigationItem {
public type = ''
public options: NavigationItemOptions = { ...defOptions }
constructor (i?: NavigationItemInput) {
this.apply(i)
}
apply (i?: NavigationItemInput): void {
if (!i) return
Apply(this, i, String, 'type')
if (i.options) {
this.options = { ...this.options, ...i.options }
}
}
}
@@ -505,6 +505,56 @@ comment:
footnote: 'Field value will be used as reference'
navigation:
chart: Charts
label: Navigation
displayOptions: Display options
appearance: Appearance
alignment: Alignment
fillJustify: Fill and Justify
fieldLabel: Label
enabled: Enabled
openIn: Open in
navigationItems: Navigation items
type: Type
disabled: Disabled
color: Color
background: Background
url: URL
drop: Dropdown position
add: Add
composePage: Compose page
tabs: Tabs
pills: Pills
small: Small
center: Center
left: Left
right: Right
bottom: Bottom
top: Top
fill: Fill
justify: Justify
primary: Primary
secondary: Secondary
success: Success
warning: Warning
danger: Danger
info: Info
text: Text
dropdown: Dropdown
multi: Multiline
sameTab: Same tab
selectOpenInType: Select open in type
newTab: New tab
dropdownItems: Dropdown items
delimiter: Delimiter
none: None
displaySubPages: Display sub-pages
selectBackground: Select background
preview:
preview: Preview
label: Label
home: Home
dropdown: dropdown
about: about
report:
label: Report
scenario: