123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- <template lang="pug">
- q-layout(view='hHh lpR fFf', container)
- q-header.card-header.q-px-md.q-py-sm
- q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='md')
- span {{t(`navEdit.editMenuItems`)}}
- q-space
- transition(name='syncing')
- q-spinner-tail.q-mr-sm(
- v-show='state.loading > 0'
- color='accent'
- size='24px'
- )
- q-btn.q-mr-sm(
- flat
- rounded
- color='white'
- :aria-label='t(`common.actions.viewDocs`)'
- icon='las la-question-circle'
- :href='siteStore.docsBase + `/admin/editors/markdown`'
- target='_blank'
- type='a'
- )
- q-btn-group(push)
- q-btn(
- push
- color='white'
- text-color='grey-7'
- :label='t(`common.actions.cancel`)'
- :aria-label='t(`common.actions.cancel`)'
- icon='las la-times'
- @click='close'
- )
- q-btn(
- push
- color='positive'
- text-color='white'
- :label='t(`common.actions.save`)'
- :aria-label='t(`common.actions.save`)'
- icon='las la-check'
- :disabled='state.loading > 0'
- @click='save'
- )
- q-drawer.bg-dark-6(:model-value='true', :width='295', dark)
- q-scroll-area.nav-edit(
- :thumb-style='thumbStyle'
- :bar-style='barStyle'
- )
- sortable(
- class='q-list q-list--dense q-list--dark nav-edit-list'
- :list='state.items'
- item-key='id'
- :options='sortableOptions'
- @end='updateItemPosition'
- )
- template(#item='{element}')
- .nav-edit-item.nav-edit-item-header(
- v-if='element.type === `header`'
- :class='state.selected === element.id ? `is-active` : ``'
- @click='setItem(element)'
- )
- q-item-label.text-caption(
- header
- ) {{ element.label }}
- q-space
- q-item-section(side)
- q-icon.handle(name='mdi-drag-horizontal', size='sm')
- q-item.nav-edit-item.nav-edit-item-link(
- v-else-if='element.type === `link`'
- :class='{ "is-active": state.selected === element.id, "is-nested": element.isNested }'
- @click='setItem(element)'
- clickable
- )
- q-item-section(side)
- q-icon(:name='element.icon', color='white')
- q-item-section.text-wordbreak-all {{ element.label }}
- q-item-section(side)
- q-icon.handle(name='mdi-drag-horizontal', size='sm')
- .nav-edit-item.nav-edit-item-separator(
- v-else
- :class='state.selected === element.id ? `is-active` : ``'
- @click='setItem(element)'
- )
- q-separator(
- dark
- inset
- style='flex: 1; margin-top: 11px;'
- )
- q-item-section(side)
- q-icon.handle(name='mdi-drag-horizontal', size='sm')
- .q-pa-md.flex
- q-btn.acrylic-btn(
- style='flex: 1;'
- flat
- color='positive'
- :label='t(`common.actions.add`)'
- :aria-label='t(`common.actions.add`)'
- icon='las la-plus-circle'
- )
- q-menu(fit, :offset='[0, 10]', auto-close)
- q-list(separator)
- q-item(clickable, @click='addItem(`header`)')
- q-item-section(side)
- q-icon(name='las la-heading')
- q-item-section
- q-item-label {{t('navEdit.header')}}
- q-item(clickable, @click='addItem(`link`)')
- q-item-section(side)
- q-icon(name='las la-link')
- q-item-section
- q-item-label {{t('navEdit.link')}}
- q-item(clickable, @click='addItem(`separator`)')
- q-item-section(side)
- q-icon(name='las la-minus')
- q-item-section
- q-item-label {{t('navEdit.separator')}}
- q-btn.q-ml-sm.acrylic-btn(
- flat
- color='grey'
- :aria-label='t(`common.actions.add`)'
- icon='las la-ellipsis-v'
- padding='xs sm'
- )
- q-menu(:offset='[0, 10]' anchor='bottom right' self='top right' auto-close)
- q-list(separator)
- q-item(clickable, @click='clearItems', :disable='state.items.length < 1')
- q-item-section(side)
- q-icon(name='las la-trash-alt', color='negative')
- q-item-section
- q-item-label {{t('navEdit.clearItems')}}
- //- q-item(clickable)
- //- q-item-section(side)
- //- q-icon(name='mdi-import')
- //- q-item-section
- //- q-item-label Copy from...
- q-page-container
- q-page.q-pa-md
- template(v-if='state.items.length < 1')
- q-card
- q-card-section
- q-icon.q-mr-sm(name='las la-arrow-left', size='xs')
- span {{ t('navEdit.emptyMenuText') }}
- template(v-else-if='!state.selected')
- q-card
- q-card-section
- q-icon.q-mr-sm(name='las la-arrow-left', size='xs')
- span {{ t('navEdit.noSelection') }}
- template(v-if='state.current.type === `header`')
- q-card.q-pb-sm
- q-card-section
- .text-subtitle1 {{t('navEdit.header')}}
- q-item
- blueprint-icon(icon='typography')
- q-item-section
- q-item-label {{t(`navEdit.label`)}}
- q-item-label(caption) {{t(`navEdit.labelHint`)}}
- q-item-section
- q-input(
- outlined
- v-model='state.current.label'
- dense
- hide-bottom-space
- :aria-label='t(`navEdit.label`)'
- )
- q-item
- blueprint-icon(icon='user-groups')
- q-item-section
- q-item-label {{t(`navEdit.visibility`)}}
- q-item-label(caption) {{t(`navEdit.visibilityHint`)}}
- q-item-section(avatar)
- q-btn-toggle(
- v-model='state.current.visibilityLimited'
- push
- glossy
- no-caps
- toggle-color='primary'
- :options='visibilityOptions'
- )
- q-item.items-center(v-if='state.current.visibilityLimited')
- q-space
- .text-caption.q-mr-md {{ t('navEdit.selectGroups') }}
- q-select(
- style='width: 100%; max-width: calc(50% - 34px);'
- outlined
- v-model='state.current.visibilityGroups'
- :options='state.groups'
- option-value='id'
- option-label='name'
- emit-value
- map-options
- dense
- multiple
- :aria-label='t(`navEdit.selectGroups`)'
- )
- q-card.q-pa-md.q-mt-md.flex
- q-space
- q-btn.acrylic-btn(
- flat
- icon='las la-trash-alt'
- :label='t(`common.actions.delete`)'
- color='negative'
- padding='xs md'
- @click='removeItem(state.current.id)'
- )
- template(v-if='state.current.type === `link`')
- q-card.q-pb-sm
- q-card-section
- .text-subtitle1 {{t('navEdit.link')}}
- q-item
- blueprint-icon(icon='typography')
- q-item-section
- q-item-label {{t(`navEdit.label`)}}
- q-item-label(caption) {{t(`navEdit.labelHint`)}}
- q-item-section
- q-input(
- outlined
- v-model='state.current.label'
- dense
- hide-bottom-space
- :aria-label='t(`navEdit.label`)'
- )
- q-separator.q-my-sm(inset)
- q-item
- blueprint-icon(icon='spring')
- q-item-section
- q-item-label {{t(`navEdit.icon`)}}
- q-item-label(caption) {{t(`navEdit.iconHint`)}}
- q-item-section
- q-input(
- outlined
- v-model='state.current.icon'
- dense
- :aria-label='t(`navEdit.icon`)'
- )
- template(#append)
- q-icon.cursor-pointer(
- name='las la-icons'
- color='primary'
- )
- q-menu(content-class='shadow-7')
- .q-pa-lg: em [ TODO: Icon Picker Dialog ]
- // icon-picker-dialog(v-model='pageStore.icon')
- q-separator.q-my-sm(inset)
- q-item
- blueprint-icon(icon='link')
- q-item-section
- q-item-label {{t(`navEdit.target`)}}
- q-item-label(caption) {{t(`navEdit.targetHint`)}}
- q-item-section
- q-input(
- outlined
- v-model='state.current.target'
- dense
- hide-bottom-space
- :aria-label='t(`navEdit.target`)'
- )
- q-separator.q-my-sm(inset)
- q-item(tag='label')
- blueprint-icon(icon='external-link')
- q-item-section
- q-item-label {{t(`navEdit.openInNewWindow`)}}
- q-item-label(caption) {{t(`navEdit.openInNewWindowHint`)}}
- q-item-section(avatar)
- q-toggle(
- v-model='state.current.openInNewWindow'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :aria-label='t(`navEdit.openInNewWindow`)'
- )
- q-separator.q-my-sm(inset)
- q-item
- blueprint-icon(icon='user-groups')
- q-item-section
- q-item-label {{t(`navEdit.visibility`)}}
- q-item-label(caption) {{t(`navEdit.visibilityHint`)}}
- q-item-section(avatar)
- q-btn-toggle(
- v-model='state.current.visibilityLimited'
- push
- glossy
- no-caps
- toggle-color='primary'
- :options='visibilityOptions'
- )
- q-item.items-center(v-if='state.current.visibilityLimited')
- q-space
- .text-caption.q-mr-md {{ t('navEdit.selectGroups') }}
- q-select(
- style='width: 100%; max-width: calc(50% - 34px);'
- outlined
- v-model='state.current.visibilityGroups'
- :options='state.groups'
- option-value='id'
- option-label='name'
- emit-value
- map-options
- dense
- multiple
- :aria-label='t(`navEdit.selectGroups`)'
- )
- q-card.q-pa-md.q-mt-md.flex.items-start
- div
- q-btn.acrylic-btn(
- v-if='state.current.isNested'
- flat
- :label='t(`navEdit.unnestItem`)'
- icon='mdi-format-indent-decrease'
- color='teal'
- padding='xs md'
- @click='state.current.isNested = false'
- )
- q-btn.acrylic-btn(
- v-else
- flat
- :label='t(`navEdit.nestItem`)'
- icon='mdi-format-indent-increase'
- color='teal'
- padding='xs md'
- @click='state.current.isNested = true'
- )
- .text-caption.q-mt-md.text-grey-7 {{ t('navEdit.nestingWarn') }}
- q-space
- q-btn.acrylic-btn(
- flat
- icon='las la-trash-alt'
- :label='t(`common.actions.delete`)'
- color='negative'
- padding='xs md'
- @click='removeItem(state.current.id)'
- )
- template(v-if='state.current.type === `separator`')
- q-card.q-pb-sm
- q-card-section
- .text-subtitle1 {{t('navEdit.separator')}}
- q-item
- blueprint-icon(icon='user-groups')
- q-item-section
- q-item-label {{t(`navEdit.visibility`)}}
- q-item-label(caption) {{t(`navEdit.visibilityHint`)}}
- q-item-section(avatar)
- q-btn-toggle(
- v-model='state.current.visibilityLimited'
- push
- glossy
- no-caps
- toggle-color='primary'
- :options='visibilityOptions'
- )
- q-item.items-center(v-if='state.current.visibilityLimited')
- q-space
- .text-caption.q-mr-md {{ t('navEdit.selectGroups') }}
- q-select(
- style='width: 100%; max-width: calc(50% - 34px);'
- outlined
- v-model='state.current.visibilityGroups'
- :options='state.groups'
- option-value='id'
- option-label='name'
- emit-value
- map-options
- dense
- multiple
- :aria-label='t(`navEdit.selectGroups`)'
- )
- q-card.q-pa-md.q-mt-md.flex
- q-space
- q-btn.acrylic-btn(
- flat
- icon='las la-trash-alt'
- :label='t(`common.actions.delete`)'
- color='negative'
- padding='xs md'
- @click='removeItem(state.current.id)'
- )
- </template>
- <script setup>
- import { useI18n } from 'vue-i18n'
- import { useQuasar } from 'quasar'
- import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
- import { v4 as uuid } from 'uuid'
- import gql from 'graphql-tag'
- import { cloneDeep, last, pick } from 'lodash-es'
- import { usePageStore } from 'src/stores/page'
- import { useSiteStore } from 'src/stores/site'
- import { Sortable } from 'sortablejs-vue3'
- import IconPickerDialog from 'src/components/IconPickerDialog.vue'
- // QUASAR
- const $q = useQuasar()
- // STORES
- const pageStore = usePageStore()
- const siteStore = useSiteStore()
- // I18N
- const { t } = useI18n()
- // DATA
- const state = reactive({
- loading: 0,
- selected: null,
- items: [],
- current: {
- label: '',
- icon: '',
- target: '/',
- openInNewWindow: false,
- visibilityGroups: [],
- visibilityLimited: false,
- isNested: false
- },
- groups: []
- })
- const sortableOptions = {
- handle: '.handle',
- animation: 150
- }
- const visibilityOptions = [
- { value: false, label: t('navEdit.visibilityAll') },
- { value: true, label: t('navEdit.visibilityLimited') }
- ]
- const thumbStyle = {
- right: '2px',
- borderRadius: '5px',
- backgroundColor: '#FFF',
- width: '5px',
- opacity: 0.5
- }
- const barStyle = {
- backgroundColor: '#000',
- width: '9px',
- opacity: 0.1
- }
- // METHODS
- function setItem (item) {
- state.selected = item.id
- state.current = item
- }
- function addItem (type) {
- const newItem = {
- id: uuid(),
- type,
- visibilityGroups: [],
- visibilityLimited: false
- }
- switch (type) {
- case 'header': {
- newItem.label = t('navEdit.header')
- break
- }
- case 'link': {
- newItem.label = t('navEdit.link')
- newItem.icon = 'mdi-text-box-outline'
- newItem.target = '/'
- newItem.openInNewWindow = false
- newItem.isNested = false
- break
- }
- }
- state.items.push(newItem)
- state.selected = newItem.id
- state.current = newItem
- }
- function removeItem (id) {
- state.items = state.items.filter(item => item.id !== id)
- state.selected = null
- state.current = {}
- }
- function clearItems () {
- state.items = []
- state.selected = null
- state.current = {}
- }
- function updateItemPosition (ev) {
- const item = state.items.splice(ev.oldIndex, 1)[0]
- state.items.splice(ev.newIndex, 0, item)
- }
- function close () {
- siteStore.$patch({ overlay: '' })
- }
- async function loadGroups () {
- state.loading++
- const resp = await APOLLO_CLIENT.query({
- query: gql`
- query getGroupsForEditNavMenu {
- groups {
- id
- name
- }
- }
- `,
- fetchPolicy: 'network-only'
- })
- state.groups = cloneDeep(resp?.data?.groups ?? [])
- state.loading--
- }
- async function loadMenuItems () {
- state.loading++
- $q.loading.show()
- try {
- const resp = await APOLLO_CLIENT.query({
- query: gql`
- query getItemsForEditNavMenu (
- $id: UUID!
- ) {
- navigationById (
- id: $id
- ) {
- id
- type
- label
- icon
- target
- openInNewWindow
- visibilityGroups
- children {
- id
- type
- label
- icon
- target
- openInNewWindow
- visibilityGroups
- }
- }
- }
- `,
- variables: {
- id: pageStore.isHome ? pageStore.navigationId : pageStore.id
- },
- fetchPolicy: 'network-only'
- })
- for (const item of cloneDeep(resp?.data?.navigationById ?? [])) {
- state.items.push({
- ...pick(item, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow', 'visibilityGroups']),
- visibilityLimited: item.visibilityGroups?.length > 0
- })
- for (const child of (item?.children ?? [])) {
- state.items.push({
- ...pick(child, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow', 'visibilityGroups']),
- visibilityLimited: item.visibilityGroups?.length > 0,
- isNested: true
- })
- }
- }
- } catch (err) {
- console.error(err)
- $q.notify({
- type: 'negative',
- message: err.message
- })
- close()
- }
- $q.loading.hide()
- state.loading--
- }
- function cleanMenuItem (item, isNested = false) {
- switch (item.type) {
- case 'header': {
- return {
- ...pick(item, ['id', 'type', 'label']),
- visibilityGroups: item.visibilityLimited ? item.visibilityGroups : []
- }
- }
- case 'link': {
- return {
- ...pick(item, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow']),
- visibilityGroups: item.visibilityLimited ? item.visibilityGroups : [],
- ...!isNested && { children: [] }
- }
- }
- case 'separator': {
- return {
- ...pick(item, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow']),
- visibilityGroups: item.visibilityLimited ? item.visibilityGroups : []
- }
- }
- }
- }
- async function save () {
- state.loading++
- $q.loading.show()
- try {
- const items = []
- for (const item of state.items) {
- if (item.isNested) {
- if (items.length < 1 || last(items)?.type !== 'link') {
- throw new Error('One or more nested link items are not under a parent link!')
- }
- items[items.length - 1].children.push(cleanMenuItem(item, true))
- } else {
- items.push(cleanMenuItem(item))
- }
- }
- const resp = await APOLLO_CLIENT.mutate({
- mutation: gql`
- mutation updateMenuItems (
- $pageId: UUID!
- $mode: NavigationMode!
- $items: [NavigationItemInput!]
- ) {
- updateNavigation (
- pageId: $pageId
- mode: $mode
- items: $items
- ) {
- operation {
- succeeded
- message
- }
- }
- }
- `,
- variables: {
- pageId: pageStore.id,
- mode: siteStore.overlayOpts.mode,
- items
- }
- })
- if (resp?.data?.updateNavigation?.operation?.succeeded) {
- $q.notify({
- type: 'positive',
- message: t('navEdit.saveSuccess')
- })
- siteStore.nav.items = items
- // -> Clear GraphQL Cache
- APOLLO_CLIENT.cache.evict('ROOT_QUERY')
- APOLLO_CLIENT.cache.gc()
- close()
- } else {
- throw new Error(resp?.data?.updateNavigation?.operation?.message || 'Unexpected error occured.')
- }
- } catch (err) {
- $q.notify({
- type: 'negative',
- message: err.message
- })
- }
- $q.loading.hide()
- state.loading--
- }
- onMounted(() => {
- loadMenuItems()
- loadGroups()
- })
- onBeforeUnmount(() => {
- siteStore.overlayOpts = {}
- })
- </script>
- <style lang="scss" scoped>
- .nav-edit {
- height: 100%;
- .handle {
- cursor: grab;
- }
- }
- .nav-edit-item {
- position: relative;
- &.is-active {
- background-color: $blue-8;
- }
- &.sortable-chosen {
- background-color: $blue-5;
- }
- }
- .nav-edit-item-header {
- display: flex;
- cursor: pointer;
- }
- .nav-edit-item-link {
- &.is-nested {
- border-left: 10px solid $dark-1;
- background-color: $dark-4;
- &.is-active {
- background-color: $primary;
- }
- & + div:not(.is-nested) {
- &::before {
- content: '';
- display: 'block';
- position: absolute;
- top: 0;
- left: 0;
- width: 10px;
- height: 10px;
- border-style: solid;
- border-color: $dark-1 transparent transparent $dark-1;
- border-width: 10px 10px 10px 0;
- }
- }
- }
- &:not(.is-nested) + &.is-nested {
- &::before {
- content: '';
- display: 'block';
- position: absolute;
- top: -10px;
- left: -10px;
- width: 10px;
- height: 10px;
- border-style: solid;
- border-color: transparent transparent $dark-1 $dark-1;
- border-width: 0 10px 10px 0;
- }
- }
- }
- .nav-edit-item-separator {
- display: flex;
- cursor: pointer;
- }
- .nav-edit-item-header, .nav-edit-item-separator {
- & + .nav-edit-item-link.is-nested {
- background-color: $negative !important;
- border-left-color: darken($negative, 10%) !important;
- & + div:not(.is-nested) {
- &::before {
- display: none !important;
- }
- }
- }
- }
- .nav-edit-list {
- .nav-edit-item-separator + .nav-edit-item-header > .q-item__label {
- padding-top: 8px;
- }
- .is-nested:first-child {
- background-color: $negative !important;
- border-left-color: darken($negative, 10%) !important;
- & + div:not(.is-nested) {
- &::before {
- display: none !important;
- }
- }
- }
- }
- </style>
|