diff --git a/server/locales/en.json b/server/locales/en.json
index 0fe2135f..e82cfe26 100644
--- a/server/locales/en.json
+++ b/server/locales/en.json
@@ -1683,6 +1683,25 @@
"history.restore.confirmText": "Are you sure you want to restore this page content as it was on {date}? This version will be copied on top of the current history. As such, newer versions will still be preserved.",
"history.restore.confirmTitle": "Restore page version?",
"history.restore.success": "Page version restored succesfully!",
+ "navEdit.editMenuItems": "Edit Menu Items",
+ "navEdit.header": "Header",
+ "navEdit.icon": "Icon",
+ "navEdit.iconHint": "Icon to display to the left of the menu item.",
+ "navEdit.label": "Label",
+ "navEdit.labelHint": "Text to display on the menu item.",
+ "navEdit.link": "Link",
+ "navEdit.nestItem": "Nest Item",
+ "navEdit.openInNewWindow": "Open in New Window",
+ "navEdit.openInNewWindowHint": "Whether the link should open in a new window / tab.",
+ "navEdit.separator": "Separator",
+ "navEdit.target": "Target",
+ "navEdit.targetHint": "Target path or external link to point to.",
+ "navEdit.title": "Edit Navigation",
+ "navEdit.unnestItem": "Unnest Item",
+ "navEdit.visibility": "Visibility",
+ "navEdit.visibilityAll": "Everyone",
+ "navEdit.visibilityHint": "Whether to show the menu item to everyone or just selected groups.",
+ "navEdit.visibilityLimited": "Selected Groups",
"pageDeleteDialog.confirm": "Are you sure you want to delete the page {name}?",
"pageDeleteDialog.deleteSuccess": "Page deleted successfully.",
"pageDeleteDialog.pageId": "Page ID {id}",
diff --git a/ux/package-lock.json b/ux/package-lock.json
index e932771e..6f6ce671 100644
--- a/ux/package-lock.json
+++ b/ux/package-lock.json
@@ -84,6 +84,8 @@
"quasar": "2.12.1",
"slugify": "1.6.6",
"socket.io-client": "4.7.1",
+ "sortablejs": "1.15.0",
+ "sortablejs-vue3": "1.2.9",
"tabulator-tables": "5.5.0",
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
@@ -6920,8 +6922,26 @@
}
},
"node_modules/sortablejs": {
- "version": "1.14.0",
- "license": "MIT"
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
+ "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
+ },
+ "node_modules/sortablejs-vue3": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/sortablejs-vue3/-/sortablejs-vue3-1.2.9.tgz",
+ "integrity": "sha512-l0IIBdu+nRIwC2+KOkiavXw5vRfsn6MIPVSVSf7ItBevcuRZ4mVzC7dgnr/Hs/VPH2Q+nF2PYP3FsrnrG+7qCw==",
+ "dependencies": {
+ "sortablejs": "^1.15.0",
+ "vue": "^3.2.37"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/MaxLeiter/"
+ },
+ "peerDependencies": {
+ "sortablejs": "^1.15.0",
+ "vue": "^3.2.25"
+ }
},
"node_modules/source-map": {
"version": "0.6.1",
@@ -7731,6 +7751,11 @@
"vue": "^3.0.1"
}
},
+ "node_modules/vuedraggable/node_modules/sortablejs": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+ "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"dev": true,
diff --git a/ux/public/_assets/icons/fluent-sidebar-menu.svg b/ux/public/_assets/icons/fluent-sidebar-menu.svg
new file mode 100644
index 00000000..2b7d00c7
--- /dev/null
+++ b/ux/public/_assets/icons/fluent-sidebar-menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/public/_assets/icons/ultraviolet-external-link.svg b/ux/public/_assets/icons/ultraviolet-external-link.svg
new file mode 100644
index 00000000..d763104a
--- /dev/null
+++ b/ux/public/_assets/icons/ultraviolet-external-link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/public/_assets/icons/ultraviolet-spring.svg b/ux/public/_assets/icons/ultraviolet-spring.svg
new file mode 100644
index 00000000..e8910175
--- /dev/null
+++ b/ux/public/_assets/icons/ultraviolet-spring.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/public/_assets/icons/ultraviolet-typography.svg b/ux/public/_assets/icons/ultraviolet-typography.svg
new file mode 100644
index 00000000..8c7d423f
--- /dev/null
+++ b/ux/public/_assets/icons/ultraviolet-typography.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/public/_assets/icons/ultraviolet-user-groups.svg b/ux/public/_assets/icons/ultraviolet-user-groups.svg
new file mode 100644
index 00000000..0a87dbcb
--- /dev/null
+++ b/ux/public/_assets/icons/ultraviolet-user-groups.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/src/components/MainOverlayDialog.vue b/ux/src/components/MainOverlayDialog.vue
index 1b75fff8..2d1d67a0 100644
--- a/ux/src/components/MainOverlayDialog.vue
+++ b/ux/src/components/MainOverlayDialog.vue
@@ -27,6 +27,10 @@ const overlays = {
loader: () => import('./FileManager.vue'),
loadingComponent: LoadingGeneric
}),
+ NavEdit: defineAsyncComponent({
+ loader: () => import('./NavEditOverlay.vue'),
+ loadingComponent: LoadingGeneric
+ }),
TableEditor: defineAsyncComponent({
loader: () => import('./TableEditorOverlay.vue'),
loadingComponent: LoadingGeneric
diff --git a/ux/src/components/NavEditMenu.vue b/ux/src/components/NavEditMenu.vue
new file mode 100644
index 00000000..7e2d79b7
--- /dev/null
+++ b/ux/src/components/NavEditMenu.vue
@@ -0,0 +1,88 @@
+
+q-card(style='min-width: 350px')
+ q-card-section.card-header
+ q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='sm')
+ span {{t(`navEdit.title`)}}
+ q-card-section
+ q-btn.full-width(
+ unelevated
+ icon='mdi-playlist-edit'
+ color='deep-orange-9'
+ :label='t(`navEdit.editMenuItems`)'
+ @click='startEditing'
+ )
+ q-separator(inset)
+ q-card-section.q-pb-none.text-body2 Mode
+ q-list(padding)
+ q-item(tag='label')
+ q-item-section(side)
+ q-radio(v-model='state.mode', val='inherit')
+ q-item-section
+ q-item-label Inherit
+ q-item-label(caption) Use the menu items and settings from the parent path.
+ q-item(tag='label')
+ q-item-section(side)
+ q-radio(v-model='state.mode', val='starting')
+ q-item-section
+ q-item-label Override Current + Descendants
+ q-item-label(caption) Set menu items and settings for this path and all children.
+ q-item(tag='label')
+ q-item-section(side)
+ q-radio(v-model='state.mode', val='exact')
+ q-item-section
+ q-item-label Override Current Only
+ q-item-label(caption) Set menu items and settings only for this path.
+ q-card-actions.card-actions
+ q-space
+ q-btn.acrylic-btn(
+ flat
+ :label='t(`common.actions.cancel`)'
+ color='grey'
+ padding='xs md'
+ @click='props.menuHideHandler'
+ )
+ q-btn(
+ unelevated
+ :label='t(`common.actions.save`)'
+ color='positive'
+ padding='xs md'
+ )
+
+
+
diff --git a/ux/src/components/NavEditOverlay.vue b/ux/src/components/NavEditOverlay.vue
new file mode 100644
index 00000000..ef013995
--- /dev/null
+++ b/ux/src/components/NavEditOverlay.vue
@@ -0,0 +1,518 @@
+
+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
+ 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'
+ )
+
+ 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'
+ )
+ 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
+ q-btn.full-width.acrylic-btn(
+ 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]')
+ q-list(separator)
+ q-item(clickable)
+ q-item-section(side)
+ q-icon(name='las la-heading')
+ q-item-section
+ q-item-label Header
+ q-item(clickable)
+ q-item-section(side)
+ q-icon(name='las la-link')
+ q-item-section
+ q-item-label {{t('navEdit.link')}}
+ q-item(clickable)
+ q-item-section(side)
+ q-icon(name='las la-minus')
+ q-item-section
+ q-item-label Separator
+ q-item(clickable, style='border-top-width: 5px;')
+ 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.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-card.q-pa-md.q-mt-md.flex
+ q-space
+ q-btn.acrylic-btn(
+ flat
+ :label='t(`common.actions.delete`)'
+ color='negative'
+ padding='xs md'
+ @click=''
+ )
+
+ 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-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(v-if='state.current.visibilityLimited')
+ q-item-section
+ q-item-section
+ q-select(
+ outlined
+ v-model='state.current.visibility'
+ :options='state.groups'
+ option-value='value'
+ option-label='label'
+ emit-value
+ map-options
+ dense
+ options-dense
+ :virtual-scroll-slice-size='1000'
+ :aria-label='t(`admin.general.uploadConflictBehavior`)'
+ )
+
+ q-card.q-pa-md.q-mt-md.flex
+ 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'
+ )
+ q-space
+ q-btn.acrylic-btn(
+ flat
+ :label='t(`common.actions.delete`)'
+ color='negative'
+ padding='xs md'
+ @click=''
+ )
+
+ template(v-if='state.current.type === `separator`')
+ q-card
+ q-card-section
+ .text-subtitle1 {{t('navEdit.separator')}}
+ q-card.q-pa-md.q-mt-md.flex
+ q-space
+ q-btn.acrylic-btn(
+ flat
+ :label='t(`common.actions.delete`)'
+ color='negative'
+ padding='xs md'
+ @click=''
+ )
+
+
+
+
+
+
diff --git a/ux/src/components/NavSidebar.vue b/ux/src/components/NavSidebar.vue
new file mode 100644
index 00000000..d4aef31d
--- /dev/null
+++ b/ux/src/components/NavSidebar.vue
@@ -0,0 +1,79 @@
+
+q-scroll-area.sidebar-nav(
+ :thumb-style='thumbStyle'
+ :bar-style='barStyle'
+ )
+ q-list(
+ clickable
+ dense
+ dark
+ )
+ q-item-label.text-blue-2.text-caption(header) Header
+ q-item(to='/install')
+ q-item-section(side)
+ q-icon(name='las la-dog', color='white')
+ q-item-section Link 1
+ q-item(to='/install')
+ q-item-section(side)
+ q-icon(name='las la-cat', color='white')
+ q-item-section Link 2
+ q-separator.q-my-sm(dark)
+ q-item(to='/install')
+ q-item-section(side)
+ q-icon(name='mdi-fruit-grapes', color='white')
+ q-item-section.text-wordbreak-all Link 3
+
+
+
+
+
diff --git a/ux/src/css/app.scss b/ux/src/css/app.scss
index 6a51c324..b8c55b08 100644
--- a/ux/src/css/app.scss
+++ b/ux/src/css/app.scss
@@ -36,6 +36,10 @@ body::-webkit-scrollbar-thumb {
font-family: 'Roboto Mono', Consolas, "Liberation Mono", Courier, monospace;
}
+.text-wordbreak-all {
+ word-break: break-all;
+}
+
// ------------------------------------------------------------------
// THEME COLORS
// ------------------------------------------------------------------
diff --git a/ux/src/layouts/MainLayout.vue b/ux/src/layouts/MainLayout.vue
index 1a2f1e2e..49c250b7 100644
--- a/ux/src/layouts/MainLayout.vue
+++ b/ux/src/layouts/MainLayout.vue
@@ -30,40 +30,27 @@ q-layout(view='hHh Lpr lff')
aria-label='Browse'
size='sm'
)
- q-scroll-area.sidebar-nav(
- :thumb-style='thumbStyle'
- :bar-style='barStyle'
- )
- q-list(
- clickable
- dense
- dark
- )
- q-item-label.text-blue-2.text-caption(header) Header
- q-item(to='/install')
- q-item-section(side)
- q-icon(name='las la-dog', color='white')
- q-item-section Link 1
- q-item(to='/install')
- q-item-section(side)
- q-icon(name='las la-cat', color='white')
- q-item-section Link 2
- q-separator.q-my-sm(dark)
- q-item(to='/install')
- q-item-section(side)
- q-icon(name='mdi-fruit-grapes', color='white')
- q-item-section Link 3
- q-bar.bg-blue-9.text-white(dense, v-if='flagsStore.experimental && userStore.authenticated')
+ nav-sidebar
+ q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
q-btn.col(
icon='las la-dharmachakra'
- label='History'
+ label='Edit Nav'
flat
- )
+ )
+ q-menu(
+ ref='navEditMenu'
+ anchor='top left'
+ self='bottom left'
+ :offset='[0, 10]'
+ )
+ nav-edit-menu(:menu-hide-handler='navEditMenu.hide')
+
q-separator(vertical)
q-btn.col(
icon='las la-bookmark'
label='Bookmarks'
flat
+ disabled
)
q-page-container
router-view
@@ -99,6 +86,8 @@ import { useUserStore } from 'src/stores/user'
import FooterNav from 'src/components/FooterNav.vue'
import HeaderNav from 'src/components/HeaderNav.vue'
import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue'
+import NavSidebar from 'src/components/NavSidebar.vue'
+import NavEditMenu from 'src/components/NavEditMenu.vue'
import MainOverlayDialog from 'src/components/MainOverlayDialog.vue'
// QUASAR
@@ -128,23 +117,9 @@ useMeta({
titleTemplate: title => `${title} - ${siteStore.title}`
})
-// DATA
+// REFS
-const leftDrawerOpen = ref(true)
-const search = ref('')
-
-const thumbStyle = {
- right: '2px',
- borderRadius: '5px',
- backgroundColor: '#FFF',
- width: '5px',
- opacity: 0.5
-}
-const barStyle = {
- backgroundColor: '#000',
- width: '9px',
- opacity: 0.1
-}
+const navEditMenu = ref(null)
// COMPUTED
@@ -152,12 +127,6 @@ const isSidebarShown = computed(() => {
return siteStore.showSideNav && !siteStore.sideNavIsDisabled && !(editorStore.isActive && editorStore.hideSideNav)
})
-// METHODS
-
-function openFileManager () {
- siteStore.openFileManager()
-}
-