From 3ca72ccc1e77367963a64d4d5ee9b3430853ebb8 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 2 Feb 2020 16:26:44 -0500 Subject: [PATCH] feat: new nav UI (wip) --- client/components/admin/admin-navigation.vue | 412 ++++++++++++------ client/components/common/page-selector.vue | 2 +- .../navigation-mutation-save-tree.gql | 12 - .../navigation/navigation-query-tree.gql | 12 - .../themes/default/components/nav-sidebar.vue | 109 ++++- server/graph/resolvers/navigation.js | 2 +- server/graph/schemas/navigation.graphql | 14 +- server/models/navigation.js | 21 +- 8 files changed, 404 insertions(+), 180 deletions(-) delete mode 100644 client/graph/admin/navigation/navigation-mutation-save-tree.gql delete mode 100644 client/graph/admin/navigation/navigation-query-tree.gql diff --git a/client/components/admin/admin-navigation.vue b/client/components/admin/admin-navigation.vue index c80f0d94..c2318f4b 100644 --- a/client/components/admin/admin-navigation.vue +++ b/client/components/admin/admin-navigation.vue @@ -14,173 +14,266 @@ v-icon(left) mdi-check span {{$t('common:actions.apply')}} v-container.pa-0.mt-3(fluid, grid-list-lg) - v-layout(row) - v-flex(style='flex: 0 0 350px;') + v-row(dense) + v-col(cols='3') v-card.animated.fadeInUp - v-list.py-2(dense, nav, dark, :class='navTree.length < 1 ? "grey lighten-4" : "primary"') - v-list-item(v-if='navTree.length < 1') - v-list-item-avatar(size='24'): v-icon(color='grey') explore_off - v-list-item-content - .caption.grey--text {{$t('navigation.emptyList')}} - draggable(v-model='navTree') - template(v-for='navItem in navTree') - v-list-item( - v-if='navItem.kind === "link"' - :key='navItem.id' - :class='(navItem === current) ? "blue" : ""' - @click='selectItem(navItem)' - ) - v-list-item-avatar(size='24'): v-icon {{navItem.icon}} - v-list-item-title {{navItem.label}} - .py-2.clickable( - v-else-if='navItem.kind === "divider"' - :key='navItem.id' - :class='(navItem === current) ? "blue" : ""' - @click='selectItem(navItem)' + v-toolbar(color='teal', dark, dense, flat, height='56') + v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}} + v-list(nav, two-line) + v-list-item-group(v-model='navMode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`') + v-list-item(value='classic') + v-list-item-avatar + img(src='/svg/icon-tree-structure-dotted.svg', alt='Site Tree') + v-list-item-content + v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}} + v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}} + v-list-item-avatar + v-icon(v-if='$vuetify.theme.dark', :color='navMode === `classic` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle + v-icon(v-else, :color='navMode === `classic` ? `teal` : `grey lighten-3`') mdi-check-circle + v-list-item(value='custom') + v-list-item-avatar + img(src='/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation') + v-list-item-content + v-list-item-title {{$t('admin:navigation.modeCustom.title')}} + v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}} + v-list-item-avatar + v-icon(v-if='$vuetify.theme.dark', :color='navMode === `custom` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle + v-icon(v-else, :color='navMode === `custom` ? `teal` : `grey lighten-3`') mdi-check-circle + v-list-item(value='none') + v-list-item-avatar + img(src='/svg/icon-cancel-dotted.svg', alt='None') + v-list-item-content + v-list-item-title {{$t('admin:navigation.modeNone.title')}} + v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}} + v-list-item-avatar + v-icon(v-if='$vuetify.theme.dark', :color='navMode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle + v-icon(v-else, :color='navMode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle + v-col(cols='9', v-if='navMode === `custom`') + v-card.animated.fadeInUp.wait-p2s + v-row(no-gutters, align='stretch') + v-col(style='flex: 0 0 350px;') + v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`') + .teal.pa-2(style='margin-bottom: 1px; height: 56px;') + v-select( + v-if='locales.length > 0' + label='Locale' + hide-details + solo + flat + background-color='teal darken-2' + dark + dense + v-model='currentLang' + :items='locales' + item-text='name' + item-value='code' ) + v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;') + v-list-item(v-if='navTree.length < 1') + v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert + v-list-item-content + em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}} + draggable(v-model='navTree') + template(v-for='navItem in navTree') + v-list-item( + v-if='navItem.kind === "link"' + :key='navItem.id' + :class='(navItem === current) ? "blue" : ""' + @click='selectItem(navItem)' + ) + v-list-item-avatar(size='24'): v-icon {{navItem.icon}} + v-list-item-title {{navItem.label}} + .py-2.clickable( + v-else-if='navItem.kind === "divider"' + :key='navItem.id' + :class='(navItem === current) ? "blue" : ""' + @click='selectItem(navItem)' + ) + v-divider + v-subheader.pl-4.clickable( + v-else-if='navItem.kind === "header"' + :key='navItem.id' + :class='(navItem === current) ? "blue" : ""' + @click='selectItem(navItem)' + ) {{navItem.label}} + v-card-chin + v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;') + template(v-slot:activator='{ on }') + v-btn(v-on='on', color='primary', depressed, block) + v-icon(left) mdi-plus + span {{$t('common:actions.add')}} + v-list + v-list-item(@click='addItem("link")') + v-list-item-avatar(size='24'): v-icon mdi-link + v-list-item-title {{$t('navigation.link')}} + v-list-item(@click='addItem("header")') + v-list-item-avatar(size='24'): v-icon mdi-format-title + v-list-item-title {{$t('navigation.header')}} + v-list-item(@click='addItem("divider")') + v-list-item-avatar(size='24'): v-icon mdi-minus + v-list-item-title {{$t('navigation.divider')}} + v-col + v-card(flat, style='border-radius: 0 4px 4px 0;') + template(v-if='current.kind === "link"') + v-toolbar(height='56', color='teal lighten-1', flat, dark) + .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}} + v-spacer + v-btn.px-5(color='white', outlined, @click='deleteItem(current)') + v-icon(left) mdi-delete + span {{$t('navigation.delete', { kind: $t('navigation.link') })}} + v-card-text + v-text-field( + outlined + :label='$t("navigation.label")' + prepend-icon='mdi-format-title' + v-model='current.label' + counter='255' + ) + v-text-field( + outlined + :label='$t("navigation.icon")' + prepend-icon='mdi-dice-5' + v-model='current.icon' + hide-details + ) + .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section. + .caption.pt-3.pl-5: strong Material Design Icons + .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home] + .caption.pt-3.pl-5: strong Font Awesome 5 + .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]). + .caption.pt-3.pl-5: strong Font Awesome 4 + .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home] + v-divider + v-card-text + v-select( + outlined + :label='$t("navigation.targetType")' + prepend-icon='mdi-near-me' + :items='navTypes' + v-model='current.targetType' + hide-details + ) + v-text-field.mt-4( + v-if='current.targetType === `external`' + outlined + :label='$t("navigation.target")' + prepend-icon='mdi-near-me' + v-model='current.target' + hide-details + ) + .d-flex.align-center.mt-4(v-else-if='current.targetType === "page"') + v-btn.ml-8( + color='primary' + dark + @click='selectPage' + ) + v-icon(left) mdi-magnify + span {{$t('admin:navigation.selectPageButton')}} + .caption.ml-4.primary--text {{current.target}} + v-text-field( + v-else-if='current.targetType === `search`' + outlined + :label='$t("navigation.navType.searchQuery")' + prepend-icon='search' + v-model='current.target' + ) + v-divider + + template(v-else-if='current.kind === "header"') + v-toolbar(height='56', color='teal lighten-1', flat, dark) + .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}} + v-spacer + v-btn.px-5(color='white', outlined, @click='deleteItem(current)') + v-icon(left) mdi-delete + span {{$t('navigation.delete', { kind: $t('navigation.header') })}} + v-card-text + v-text-field( + outlined + :label='$t("navigation.label")' + prepend-icon='mdi-format-title' + v-model='current.label' + ) v-divider - v-subheader.pl-4.clickable( - v-else-if='navItem.kind === "header"' - :key='navItem.id' - :class='(navItem === current) ? "blue" : ""' - @click='selectItem(navItem)' - ) {{navItem.label}} - v-card-chin - v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;') - template(v-slot:activator='{ on }') - v-btn(v-on='on', color='primary', depressed, block) - v-icon(left) mdi-plus - span {{$t('common:actions.add')}} - v-list - v-list-item(@click='addItem("link")') - v-list-item-avatar(size='24'): v-icon mdi-link - v-list-item-title {{$t('navigation.link')}} - v-list-item(@click='addItem("header")') - v-list-item-avatar(size='24'): v-icon mdi-format-title - v-list-item-title {{$t('navigation.header')}} - v-list-item(@click='addItem("divider")') - v-list-item-avatar(size='24'): v-icon mdi-minus - v-list-item-title {{$t('navigation.divider')}} - v-flex.animated.fadeInUp.wait-p2s - v-card.wiki-form(v-if='current.kind === "link"') - v-toolbar(dense, color='blue', flat, dark).subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}} - v-card-text - v-text-field( - outlined - :label='$t("navigation.label")' - prepend-icon='mdi-format-title' - v-model='current.label' - counter='255' - ) - v-text-field( - outlined - :label='$t("navigation.icon")' - prepend-icon='mdi-dice-5' - v-model='current.icon' - hide-details - ) - .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section. - .caption.pt-3.pl-5: strong Material Design Icons - .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home] - .caption.pt-3.pl-5: strong Font Awesome 5 - .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]). - .caption.pt-3.pl-5: strong Font Awesome 4 - .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home] - v-select.mt-4( - outlined - :label='$t("navigation.targetType")' - prepend-icon='mdi-near-me' - :items='navTypes' - v-model='current.targetType' - ) - v-text-field( - v-if='current.targetType === `external`' - outlined - :label='$t("navigation.target")' - prepend-icon='mdi-near-me' - v-model='current.target' - ) - v-btn( - v-else-if='current.targetType === "page"' - color='indigo' - :dark='false' - disabled - @click='selectPage' - ) - v-icon(left) mdi-search - span Select Page... - v-text-field( - v-else-if='current.targetType === `search`' - outlined - :label='$t("navigation.navType.searchQuery")' - prepend-icon='search' - v-model='current.target' - ) - v-card-chin - v-spacer - v-btn.px-5(color='red', outlined, @click='deleteItem(current)') - v-icon(left) mdi-delete - span {{$t('navigation.delete', { kind: $t('navigation.link') })}} - v-card(v-else-if='current.kind === "header"') - v-toolbar(dense, color='blue', flat, dark) - .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}} - v-card-text - v-text-field( - outlined - :label='$t("navigation.label")' - prepend-icon='mdi-format-title' - v-model='current.label' - ) - v-card-chin - v-spacer - v-btn.px-5(color='red', outlined, @click='deleteItem(current)') - v-icon(left) mdi-delete - span {{$t('navigation.delete', { kind: $t('navigation.header') })}} - div(v-else-if='current.kind === "divider"') - v-btn.mt-0.px-5(color='red', outlined, @click='deleteItem(current)') - v-icon(left) mdi-delete - span {{$t('navigation.delete', { kind: $t('navigation.divider') })}} - v-card(v-else) - v-card-text.grey--text(v-if='navTree.length > 0') {{$t('navigation.noSelectionText')}} - v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}} + div(v-else-if='current.kind === "divider"') + v-toolbar(height='56', color='teal lighten-1', flat, dark) + .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}} + v-spacer + v-btn.px-5(color='white', outlined, @click='deleteItem(current)') + v-icon(left) mdi-delete + span {{$t('navigation.delete', { kind: $t('navigation.divider') })}} + + v-card-text(v-if='current.kind') + v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details) + v-radio(:label='$t("admin:navigation.visibilityMode.all")', value='all', color='primary') + v-radio.mt-3(:label='$t("admin:navigation.visibilityMode.restricted")', value='restricted', color='primary') + .pl-8 + v-select.pl-8.mt-3( + item-text='name' + item-value='id' + outlined + prepend-icon='mdi-account-group' + label='Groups' + :disabled='current.visibilityMode !== `restricted`' + v-model='current.visibilityGroups' + :items='groups' + persistent-hint + clearable + multiple + ) + template(v-else) + v-toolbar(height='56', color='teal lighten-1', flat, dark) + v-card-text.grey--text(v-if='navTree.length > 0') {{$t('navigation.noSelectionText')}} + v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}} + + page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang') diff --git a/server/graph/resolvers/navigation.js b/server/graph/resolvers/navigation.js index 46cd27cf..35b9a611 100644 --- a/server/graph/resolvers/navigation.js +++ b/server/graph/resolvers/navigation.js @@ -11,7 +11,7 @@ module.exports = { }, NavigationQuery: { async tree(obj, args, context, info) { - return WIKI.models.navigation.getTree() + return WIKI.models.navigation.getTree({ cache: false, locale: 'all' }) } }, NavigationMutation: { diff --git a/server/graph/schemas/navigation.graphql b/server/graph/schemas/navigation.graphql index 65702e9f..73e32429 100644 --- a/server/graph/schemas/navigation.graphql +++ b/server/graph/schemas/navigation.graphql @@ -15,7 +15,7 @@ extend type Mutation { # ----------------------------------------------- type NavigationQuery { - tree: [NavigationItem]! + tree: [NavigationTree]! } # ----------------------------------------------- @@ -24,7 +24,7 @@ type NavigationQuery { type NavigationMutation { updateTree( - tree: [NavigationItemInput]! + tree: [NavigationTreeInput]! ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"]) } @@ -32,6 +32,16 @@ type NavigationMutation { # TYPES # ----------------------------------------------- +type NavigationTree { + locale: String! + items: [NavigationItem]! +} + +input NavigationTreeInput { + locale: String! + items: [NavigationItemInput]! +} + type NavigationItem { id: String! kind: String! diff --git a/server/models/navigation.js b/server/models/navigation.js index 5bcc8b9a..d23d57ca 100644 --- a/server/models/navigation.js +++ b/server/models/navigation.js @@ -1,4 +1,5 @@ const Model = require('objection').Model +const _ = require('lodash') /* global WIKI */ @@ -21,19 +22,29 @@ module.exports = class Navigation extends Model { } } - static async getTree({ cache = false } = {}) { + static async getTree({ cache = false, locale = 'en' } = {}) { if (cache) { - const navTreeCached = await WIKI.cache.get('nav:sidebar') + const navTreeCached = await WIKI.cache.get(`nav:sidebar:${locale}`) if (navTreeCached) { return navTreeCached } } const navTree = await WIKI.models.navigation.query().findOne('key', 'site') if (navTree) { - if (cache) { - await WIKI.cache.set('nav:sidebar', navTree.config, 300) + // Check for pre-2.1 format + if (_.has(navTree.config[0], 'kind')) { + navTree.config = [{ + locale: 'en', + items: navTree.config + }] } - return navTree.config + + for (const tree of navTree.config) { + if (cache) { + await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300) + } + } + return locale === 'all' ? navTree.config : WIKI.cache.get(`nav:sidebar:${locale}`) } else { WIKI.logger.warn('Site Navigation is missing or corrupted.') return []