feat: edit navigation

pull/6775/head
NGPixel 1 year ago
parent 5eaba2cbb0
commit 607b8d8100
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -179,7 +179,6 @@ export async function up (knex) {
// NAVIGATION ---------------------------- // NAVIGATION ----------------------------
.createTable('navigation', table => { .createTable('navigation', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('name').notNullable()
table.jsonb('items').notNullable().defaultTo('[]') table.jsonb('items').notNullable().defaultTo('[]')
}) })
// PAGE HISTORY ------------------------ // PAGE HISTORY ------------------------
@ -298,6 +297,8 @@ export async function up (knex) {
table.enu('type', ['folder', 'page', 'asset']).notNullable().index() table.enu('type', ['folder', 'page', 'asset']).notNullable().index()
table.string('locale', 10).notNullable().defaultTo('en').index() table.string('locale', 10).notNullable().defaultTo('en').index()
table.string('title').notNullable() table.string('title').notNullable()
table.enum('navigationMode', ['inherit', 'override', 'overrideExact', 'hide', 'hideExact']).notNullable().defaultTo('inherit').index()
table.uuid('navigationId').index()
table.jsonb('meta').notNullable().defaultTo('{}') table.jsonb('meta').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()) table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()) table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
@ -393,7 +394,6 @@ export async function up (knex) {
table.unique(['siteId', 'tag']) table.unique(['siteId', 'tag'])
}) })
.table('tree', table => { .table('tree', table => {
table.uuid('navigationId').references('id').inTable('navigation').index()
table.uuid('siteId').notNullable().references('id').inTable('sites') table.uuid('siteId').notNullable().references('id').inTable('sites')
}) })
.table('userKeys', table => { .table('userKeys', table => {
@ -415,7 +415,6 @@ export async function up (knex) {
const groupAdminId = uuid() const groupAdminId = uuid()
const groupGuestId = '10000000-0000-4000-8000-000000000001' const groupGuestId = '10000000-0000-4000-8000-000000000001'
const navDefaultId = uuid()
const siteId = uuid() const siteId = uuid()
const authModuleId = uuid() const authModuleId = uuid()
const userAdminId = uuid() const userAdminId = uuid()
@ -568,6 +567,7 @@ export async function up (knex) {
} }
}, },
features: { features: {
browse: true,
ratings: false, ratings: false,
ratingsMode: 'off', ratingsMode: 'off',
comments: false, comments: false,
@ -622,10 +622,6 @@ export async function up (knex) {
config: {} config: {}
} }
}, },
nav: {
mode: 'mixed',
defaultId: navDefaultId,
},
theme: { theme: {
dark: false, dark: false,
codeBlocksTheme: 'github-dark', codeBlocksTheme: 'github-dark',
@ -757,13 +753,13 @@ export async function up (knex) {
// -> NAVIGATION // -> NAVIGATION
await knex('navigation').insert({ await knex('navigation').insert({
id: navDefaultId, id: siteId,
name: 'Default',
items: JSON.stringify([ items: JSON.stringify([
{ {
id: uuid(), id: uuid(),
type: 'header', type: 'header',
label: 'Sample Header' label: 'Sample Header',
visibilityGroups: []
}, },
{ {
id: uuid(), id: uuid(),
@ -772,6 +768,7 @@ export async function up (knex) {
label: 'Sample Link 1', label: 'Sample Link 1',
target: '/', target: '/',
openInNewWindow: false, openInNewWindow: false,
visibilityGroups: [],
children: [] children: []
}, },
{ {
@ -781,11 +778,13 @@ export async function up (knex) {
label: 'Sample Link 2', label: 'Sample Link 2',
target: '/', target: '/',
openInNewWindow: false, openInNewWindow: false,
visibilityGroups: [],
children: [] children: []
}, },
{ {
id: uuid(), id: uuid(),
type: 'separator', type: 'separator',
visibilityGroups: []
}, },
{ {
id: uuid(), id: uuid(),
@ -794,6 +793,7 @@ export async function up (knex) {
label: 'Sample Link 3', label: 'Sample Link 3',
target: '/', target: '/',
openInNewWindow: false, openInNewWindow: false,
visibilityGroups: [],
children: [] children: []
} }
]), ]),

@ -1,4 +1,5 @@
import { generateError, generateSuccess } from '../../helpers/graph.mjs' import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { isNil } from 'lodash-es'
export default { export default {
Query: { Query: {
@ -9,15 +10,118 @@ export default {
Mutation: { Mutation: {
async updateNavigation (obj, args, context) { async updateNavigation (obj, args, context) {
try { try {
// await WIKI.db.navigation.query().patch({ let updateInherited = false
// config: args.tree let updateInheritedNavId = null
// }).where('key', 'site') let updateNavId = null
let ancestorNavId = null
const treeEntry = await WIKI.db.knex('tree').where('id', args.pageId).first()
if (!treeEntry) {
throw new Error('Invalid ID')
}
const currentNavId = treeEntry.folderPath === '' && treeEntry.fileName === 'home' ? treeEntry.siteId : treeEntry.id
const treeEntryPath = treeEntry.folderPath ? `${treeEntry.folderPath}.${treeEntry.fileName}` : treeEntry.fileName
// -> Create / Update Nav Menu Items
if (!isNil(args.items)) {
await WIKI.db.knex('navigation').insert({
id: currentNavId,
items: JSON.stringify(args.items),
siteId: treeEntry.siteId
}).onConflict('id').merge({
items: JSON.stringify(args.items)
})
}
// -> Find ancestor nav ID
const ancNavResult = await WIKI.db.knex.raw(`
SELECT "navigationId", "navigationMode", nlevel("folderPath" || "fileName") AS levels
FROM tree
WHERE ("folderPath" || "fileName") @> :currentPath
AND "navigationMode" IN ('override', 'hide')
ORDER BY levels DESC
LIMIT 1
`, {
currentPath: treeEntry.folderPath
})
if (ancNavResult.rowCount > 0) {
ancestorNavId = ancNavResult.rows[0]?.navigationId
} else {
ancestorNavId = treeEntry.siteId
}
// -> Update mode
switch (args.mode) {
case 'inherit': {
updateNavId = ancestorNavId
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
case 'override': {
updateNavId = treeEntry.id
updateInherited = true
updateInheritedNavId = treeEntry.id
break
}
case 'overrideExact': {
updateNavId = treeEntry.id
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
case 'hide': {
updateInherited = true
updateNavId = null
break
}
case 'hideExact': {
updateNavId = null
if (['override', 'hide'].includes(treeEntry.navigationMode)) {
updateInherited = true
updateInheritedNavId = ancestorNavId
}
break
}
}
// -> Set for current path
await WIKI.db.knex('tree').where('id', treeEntry.id).update({ navigationMode: args.mode, navigationId: updateNavId })
// -> Update nodes that inherit from current
if (updateInherited) {
await WIKI.db.knex.raw(`
UPDATE tree tt
SET "navigationId" = :navId
WHERE type IN ('page', 'folder')
AND "folderPath" <@ :overridePath
AND "navigationMode" = 'inherit'
AND NOT EXISTS (
SELECT 1
FROM tree tc
WHERE type IN ('page', 'folder')
AND tc."folderPath" <@ :overridePath
AND tc."folderPath" @> tt."folderPath"
AND tc."navigationMode" IN ('override', 'hide')
)
`, {
navId: updateInheritedNavId,
overridePath: treeEntryPath
})
}
// for (const tree of args.tree) { // for (const tree of args.tree) {
// await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300) // await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)
// } // }
return { return {
responseResult: generateSuccess('Navigation updated successfully') operation: generateSuccess('Navigation updated successfully'),
navigationMode: args.mode,
navigationId: updateNavId
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)

@ -14,10 +14,10 @@ extend type Query {
extend type Mutation { extend type Mutation {
updateNavigation( updateNavigation(
id: UUID! pageId: UUID!
name: String! mode: NavigationMode!
items: [JSON]! items: [NavigationItemInput!]
): DefaultResponse ): NavigationUpdateResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -26,10 +26,42 @@ extend type Mutation {
type NavigationItem { type NavigationItem {
id: UUID id: UUID
type: String type: NavigationItemType
label: String label: String
icon: String icon: String
target: String target: String
openInNewWindow: Boolean openInNewWindow: Boolean
visibilityGroups: [UUID]
children: [NavigationItem] children: [NavigationItem]
} }
input NavigationItemInput {
id: UUID!
type: NavigationItemType!
label: String
icon: String
target: String
openInNewWindow: Boolean
visibilityGroups: [UUID!]!
children: [NavigationItemInput!]
}
enum NavigationItemType {
header
link
separator
}
enum NavigationMode {
inherit
override
overrideExact
hide
hideExact
}
type NavigationUpdateResponse {
operation: Operation
navigationMode: NavigationMode
navigationId: UUID
}

@ -238,6 +238,7 @@ type Page {
isSearchable: Boolean isSearchable: Boolean
locale: String locale: String
navigationId: UUID navigationId: UUID
navigationMode: NavigationMode
password: String password: String
path: String path: String
publishEndDate: Date publishEndDate: Date

@ -79,6 +79,7 @@ type SiteRobots {
} }
type SiteFeatures { type SiteFeatures {
browse: Boolean
ratings: Boolean ratings: Boolean
ratingsMode: SitePageRatingMode ratingsMode: SitePageRatingMode
comments: Boolean comments: Boolean
@ -194,6 +195,7 @@ input SiteRobotsInput {
} }
input SiteFeaturesInput { input SiteFeaturesInput {
browse: Boolean
ratings: Boolean ratings: Boolean
ratingsMode: SitePageRatingMode ratingsMode: SitePageRatingMode
comments: Boolean comments: Boolean

@ -204,6 +204,8 @@
"admin.flags.title": "Flags", "admin.flags.title": "Flags",
"admin.flags.warn.hint": "Doing so may result in data loss, performance issues or a broken installation!", "admin.flags.warn.hint": "Doing so may result in data loss, performance issues or a broken installation!",
"admin.flags.warn.label": "Do NOT enable these flags unless you know what you're doing!", "admin.flags.warn.label": "Do NOT enable these flags unless you know what you're doing!",
"admin.general.allowBrowse": "Allow Browsing",
"admin.general.allowBrowseHint": "Can users browse using the tree structure of the site to pages they have access to?",
"admin.general.allowComments": "Allow Comments", "admin.general.allowComments": "Allow Comments",
"admin.general.allowCommentsHint": "Can users leave comments on pages? Can be restricted using Page Rules.", "admin.general.allowCommentsHint": "Can users leave comments on pages? Can be restricted using Page Rules.",
"admin.general.allowContributions": "Allow Contributions", "admin.general.allowContributions": "Allow Contributions",
@ -1697,6 +1699,8 @@
"navEdit.noSelection": "Select a menu item from the left to start editing.", "navEdit.noSelection": "Select a menu item from the left to start editing.",
"navEdit.openInNewWindow": "Open in New Window", "navEdit.openInNewWindow": "Open in New Window",
"navEdit.openInNewWindowHint": "Whether the link should open in a new window / tab.", "navEdit.openInNewWindowHint": "Whether the link should open in a new window / tab.",
"navEdit.saveModeSuccess": "Navigation mode set successfully.",
"navEdit.saveSuccess": "Menu items saved successfully.",
"navEdit.selectGroups": "Group(s):", "navEdit.selectGroups": "Group(s):",
"navEdit.separator": "Separator", "navEdit.separator": "Separator",
"navEdit.target": "Target", "navEdit.target": "Target",
@ -1711,6 +1715,8 @@
"pageDeleteDialog.deleteSuccess": "Page deleted successfully.", "pageDeleteDialog.deleteSuccess": "Page deleted successfully.",
"pageDeleteDialog.pageId": "Page ID {id}", "pageDeleteDialog.pageId": "Page ID {id}",
"pageDeleteDialog.title": "Confirm Page Deletion", "pageDeleteDialog.title": "Confirm Page Deletion",
"pageDuplicateDialog.title": "Duplicate and Save As...",
"pageRenameDialog.title": "Rename / Move to...",
"pageSaveDialog.displayModePath": "Browse Using Paths", "pageSaveDialog.displayModePath": "Browse Using Paths",
"pageSaveDialog.displayModeTitle": "Browse Using Titles", "pageSaveDialog.displayModeTitle": "Browse Using Titles",
"pageSaveDialog.title": "Save As...", "pageSaveDialog.title": "Save As...",

@ -20,6 +20,7 @@ export class Navigation extends Model {
static async getNav ({ id, cache = false, userGroups = [] }) { static async getNav ({ id, cache = false, userGroups = [] }) {
const result = await WIKI.db.navigation.query().findById(id).select('items') const result = await WIKI.db.navigation.query().findById(id).select('items')
if (!result) { return [] }
return result.items.filter(item => { return result.items.filter(item => {
return !item.visibilityGroups?.length || intersection(item.visibilityGroups, userGroups).length > 0 return !item.visibilityGroups?.length || intersection(item.visibilityGroups, userGroups).length > 0
}).map(item => { }).map(item => {

@ -64,14 +64,6 @@ export class PageHistory extends Model {
from: 'pageHistory.authorId', from: 'pageHistory.authorId',
to: 'users.id' to: 'users.id'
} }
},
locale: {
relation: Model.BelongsToOneRelation,
modelClass: Locale,
join: {
from: 'pageHistory.locale',
to: 'locales.code'
}
} }
} }
} }

@ -1220,7 +1220,8 @@ export class Page extends Model {
creatorName: 'creator.name', creatorName: 'creator.name',
creatorEmail: 'creator.email' creatorEmail: 'creator.email'
}, },
'tree.navigationId' 'tree.navigationId',
'tree.navigationMode'
]) ])
.joinRelated('author') .joinRelated('author')
.joinRelated('creator') .joinRelated('creator')

@ -48,16 +48,7 @@ export class Site extends Model {
} }
static async createSite (hostname, config) { static async createSite (hostname, config) {
const newSiteId = uuid
const newDefaultNav = await WIKI.db.navigation.query().insertAndFetch({
name: 'Default',
siteId: newSiteId,
items: JSON.stringify([])
})
const newSite = await WIKI.db.sites.query().insertAndFetch({ const newSite = await WIKI.db.sites.query().insertAndFetch({
id: newSiteId,
hostname, hostname,
isEnabled: true, isEnabled: true,
config: defaultsDeep(config, { config: defaultsDeep(config, {
@ -76,6 +67,7 @@ export class Site extends Model {
} }
}, },
features: { features: {
browse: true,
ratings: false, ratings: false,
ratingsMode: 'off', ratingsMode: 'off',
comments: false, comments: false,
@ -148,10 +140,6 @@ export class Site extends Model {
config: {} config: {}
} }
}, },
nav: {
mode: 'mixed',
defaultId: newDefaultNav.id,
},
uploads: { uploads: {
conflictBehavior: 'overwrite', conflictBehavior: 'overwrite',
normalizeFilename: true normalizeFilename: true
@ -159,6 +147,14 @@ export class Site extends Model {
}) })
}) })
WIKI.logger.debug(`Creating new root navigation for site ${newSite.id}`)
await WIKI.db.navigation.query().insert({
id: newSite.id,
siteId: newSite.id,
items: JSON.stringify([])
})
WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`) WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`)
await WIKI.db.storage.query().insert({ await WIKI.db.storage.query().insert({

@ -147,7 +147,8 @@ export class Tree extends Model {
hash: generateHash(fullPath), hash: generateHash(fullPath),
locale: locale, locale: locale,
siteId, siteId,
meta meta,
navigationId: siteId,
}).returning('*') }).returning('*')
return pageEntry[0] return pageEntry[0]
@ -245,7 +246,8 @@ export class Tree extends Model {
siteId: siteId, siteId: siteId,
locale: locale, locale: locale,
folderPath: encodeFolderPath(parentPath), folderPath: encodeFolderPath(parentPath),
fileName: pathName fileName: pathName,
type: 'folder'
}).first() }).first()
if (existingFolder) { if (existingFolder) {
throw new Error('ERR_FOLDER_ALREADY_EXISTS') throw new Error('ERR_FOLDER_ALREADY_EXISTS')
@ -354,7 +356,8 @@ export class Tree extends Model {
.andWhere({ .andWhere({
siteId: folder.siteId, siteId: folder.siteId,
folderPath: folder.folderPath, folderPath: folder.folderPath,
fileName: pathName fileName: pathName,
type: 'folder'
}).first() }).first()
if (existingFolder) { if (existingFolder) {
throw new Error('ERR_FOLDER_ALREADY_EXISTS') throw new Error('ERR_FOLDER_ALREADY_EXISTS')

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#42a5f5" d="M42 38L17 38 17 5 34 5 42 13z"/><path fill="#e1f5fe" d="M40.5 14L33 14 33 6.5z"/><path fill="#1565c0" d="M22 19H38V21H22zM22 23H34V25H22zM22 27H38V29H22zM22 31H34V33H22z"/><g><path fill="#90caf9" d="M31 43L6 43 6 10 23 10 31 18z"/><path fill="#e1f5fe" d="M29.5 19L22 19 22 11.5z"/><path fill="#1976d2" d="M11 24H26V26H11zM11 28H22V30H11zM11 32H26V34H11zM11 36H22V38H11z"/></g></svg>

After

Width:  |  Height:  |  Size: 494 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M14.5 33.5L14.5 1.5 30.793 1.5 38.5 9.207 38.5 33.5z"/><path fill="#4788c7" d="M30.586,2L38,9.414V33H15V2H30.586 M31,1H14v33h25V9L31,1L31,1z"/><path fill="#b6dcfe" d="M30.5 9.5L30.5 1.5 30.793 1.5 38.5 9.207 38.5 9.5z"/><path fill="#4788c7" d="M31,2.414L37.586,9H31V2.414 M31,1h-1v9h9V9L31,1L31,1z"/><path fill="#fff" d="M1.5 38.5L1.5 6.5 17.793 6.5 25.5 14.207 25.5 38.5z"/><path fill="#4788c7" d="M17.586,7L25,14.414V38H2V7H17.586 M18,6H1v33h25V14L18,6L18,6z"/><path fill="#dff0fe" d="M17.5 14.5L17.5 6.5 17.793 6.5 25.5 14.207 25.5 14.5z"/><path fill="#4788c7" d="M18 7.414L24.586 14H18V7.414M18 6h-1v9h9v-1L18 6 18 6zM6.5 19h14c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-14C6.225 20 6 19.775 6 19.5l0 0C6 19.225 6.225 19 6.5 19zM6.5 25h14c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-14C6.225 26 6 25.775 6 25.5l0 0C6 25.225 6.225 25 6.5 25zM6.5 22h11c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-11C6.225 23 6 22.775 6 22.5l0 0C6 22.225 6.225 22 6.5 22zM6.5 28h11c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-11C6.225 29 6 28.775 6 28.5l0 0C6 28.225 6.225 28 6.5 28zM25.5 15h8c.275 0 .5.225.5.5v0c0 .275-.225.5-.5.5h-8c-.275 0-.5-.225-.5-.5v0C25 15.225 25.225 15 25.5 15zM25.5 21h8c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-8c-.275 0-.5-.225-.5-.5l0 0C25 21.225 25.225 21 25.5 21zM25.5 27h8c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-8c-.275 0-.5-.225-.5-.5l0 0C25 27.225 25.225 27 25.5 27zM25.5 18h5c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-5c-.275 0-.5-.225-.5-.5l0 0C25 18.225 25.225 18 25.5 18zM25.5 24h5c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-5c-.275 0-.5-.225-.5-.5l0 0C25 24.225 25.225 24 25.5 24zM6.5 31h14c.275 0 .5.225.5.5l0 0c0 .275-.225.5-.5.5h-14C6.225 32 6 31.775 6 31.5l0 0C6 31.225 6.225 31 6.5 31z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M24.5 28.5H38.5V38.5H24.5z"/><path fill="#4788c7" d="M38,29v9H25v-9H38 M39,28H24v11h15V28L39,28z"/><path fill="#dff0fe" d="M24.5 13.5H38.5V23.5H24.5z"/><path fill="#4788c7" d="M38,14v9H25v-9H38 M39,13H24v11h15V13L39,13z"/><path fill="#98ccfd" d="M1.5 1.5H15.5V11.5H1.5z"/><path fill="#4788c7" d="M15 2v9H2V2H15M16 1H1v11h15V1L16 1zM24 33L9 33 9 12 8 12 8 34 24 34z"/><path fill="#4788c7" d="M15.5 10.5H16.5V26.5H15.5z" transform="rotate(-90 16 18.5)"/></svg>

After

Width:  |  Height:  |  Size: 570 B

@ -1,8 +1,9 @@
<template lang="pug"> <template lang="pug">
q-menu.translucent-menu( q-menu.translucent-menu(
auto-close auto-close
anchor='bottom left' :anchor='props.anchor'
self='top left' :self='props.self'
:offset='props.offset'
) )
q-list(padding, style='min-width: 200px;') q-list(padding, style='min-width: 200px;')
q-item( q-item(
@ -25,6 +26,23 @@ import { useQuasar } from 'quasar'
import { useCommonStore } from 'src/stores/common' import { useCommonStore } from 'src/stores/common'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
// PROPS
const props = defineProps({
anchor: {
type: String,
default: 'bottom left'
},
self: {
type: String,
default: 'top left'
},
offset: {
type: Array,
default: () => ([0, 0])
}
})
// QUASAR // QUASAR
const $q = useQuasar() const $q = useQuasar()

@ -3,35 +3,63 @@ q-card(style='min-width: 350px')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='sm')
span {{t(`navEdit.title`)}} 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-list(padding)
template(v-if='isRoot')
q-item(tag='label')
q-item-section(side)
q-radio(v-model='state.mode', val='inherit')
q-item-section
q-item-label Show
q-item-label(caption) Show the left sidebar navigaiton menu items.
q-item(tag='label') q-item(tag='label')
q-item-section(side) q-item-section(side)
q-radio(v-model='state.mode', val='inherit', :disable='isRoot') q-radio(v-model='state.mode', val='hide')
q-item-section
q-item-label Hide
q-item-label(caption) Completely hide the left sidebar navigation.
template(v-else)
q-item(tag='label')
q-item-section(side)
q-radio(v-model='state.mode', val='inherit')
q-item-section q-item-section
q-item-label Inherit q-item-label Inherit
q-item-label(caption) Use the menu items and settings from the parent path. q-item-label(caption) Use the menu items and settings from the parent path.
q-item(tag='label') q-item(tag='label')
q-item-section(side) q-item-section(side)
q-radio(v-model='state.mode', val='starting', :disable='isRoot') q-radio(v-model='state.mode', val='override')
q-item-section q-item-section
q-item-label Override Current + Descendants q-item-label Override Current + Descendants
q-item-label(caption) Set menu items and settings for this path and all children. q-item-label(caption) Set menu items and settings for this path and all descendants.
q-item(tag='label') q-item(tag='label')
q-item-section(side) q-item-section(side)
q-radio(v-model='state.mode', val='exact', :disable='isRoot') q-radio(v-model='state.mode', val='overrideExact')
q-item-section q-item-section
q-item-label Override Current Only q-item-label Override Current Only
q-item-label(caption) Set menu items and settings only for this path. q-item-label(caption) Set menu items and settings only for this path.
q-item(tag='label')
q-item-section(side)
q-radio(v-model='state.mode', val='hide')
q-item-section
q-item-label Hide Current + Descendants
q-item-label(caption) Completely hide the left sidebar navigation for this path and all descendants.
q-item(tag='label')
q-item-section(side)
q-radio(v-model='state.mode', val='hideExact')
q-item-section
q-item-label Hide Current Only
q-item-label(caption) Completely hide the left sidebar navigation only for this path.
template(v-if='canEditMenuItems')
q-separator(inset)
q-card-section
q-btn.full-width(
unelevated
icon='mdi-playlist-edit'
color='deep-orange-9'
:label='t(`navEdit.editMenuItems`)'
@click='startEditing'
)
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
@ -46,12 +74,16 @@ q-card(style='min-width: 350px')
:label='t(`common.actions.save`)' :label='t(`common.actions.save`)'
color='positive' color='positive'
padding='xs md' padding='xs md'
@click='save'
:loading='state.loading > 0'
) )
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import gql from 'graphql-tag'
import { usePageStore } from 'src/stores/page' import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
@ -62,9 +94,17 @@ const props = defineProps({
menuHideHandler: { menuHideHandler: {
type: Function, type: Function,
default: () => ({}) default: () => ({})
},
updatePositionHandler: {
type: Function,
default: () => ({})
} }
}) })
// QUASAR
const $q = useQuasar()
// STORES // STORES
const pageStore = usePageStore() const pageStore = usePageStore()
@ -77,14 +117,94 @@ const { t } = useI18n()
// DATA // DATA
const state = reactive({ const state = reactive({
mode: 'inherit' mode: 'inherit',
loading: 0
})
// COMPUTED
const isRoot = computed(() => {
return pageStore.path === '' || pageStore.path === 'home'
})
const canEditMenuItems = computed(() => {
if (!isRoot.value && state.mode === 'inherit') { return false }
return ['inherit', 'override', 'overrideExact'].includes(state.mode)
})
// WATCHERS
watch(() => state.mode, () => {
nextTick(() => {
props.updatePositionHandler()
})
}) })
// METHODS // METHODS
function startEditing () { function startEditing () {
siteStore.$patch({ overlay: 'NavEdit' }) siteStore.$patch({ overlay: 'NavEdit', overlayOpts: { mode: state.mode } })
props.menuHideHandler() props.menuHideHandler()
} }
async function save () {
state.loading++
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation updateNavMode (
$pageId: UUID!
$mode: NavigationMode!
) {
updateNavigation (
pageId: $pageId
mode: $mode
) {
operation {
succeeded
message
}
navigationId
}
}
`,
variables: {
pageId: pageStore.id,
mode: state.mode
}
})
if (resp?.data?.updateNavigation?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('navEdit.saveModeSuccess')
})
// -> Clear GraphQL Cache
APOLLO_CLIENT.cache.evict('ROOT_QUERY')
APOLLO_CLIENT.cache.gc()
// -> Set current nav id
pageStore.$patch({
navigationMode: state.mode,
navigationId: resp.data.updateNavigation.navigationId
})
props.menuHideHandler()
} else {
throw new Error(resp?.data?.updateNavigation?.operation?.message || 'Unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(() => {
state.mode = pageStore.navigationMode
})
</script> </script>

@ -38,6 +38,7 @@ q-layout(view='hHh lpR fFf', container)
:aria-label='t(`common.actions.save`)' :aria-label='t(`common.actions.save`)'
icon='las la-check' icon='las la-check'
:disabled='state.loading > 0' :disabled='state.loading > 0'
@click='save'
) )
q-drawer.bg-dark-6(:model-value='true', :width='295', dark) q-drawer.bg-dark-6(:model-value='true', :width='295', dark)
@ -50,6 +51,7 @@ q-layout(view='hHh lpR fFf', container)
:list='state.items' :list='state.items'
item-key='id' item-key='id'
:options='sortableOptions' :options='sortableOptions'
@end='updateItemPosition'
) )
template(#item='{element}') template(#item='{element}')
.nav-edit-item.nav-edit-item-header( .nav-edit-item.nav-edit-item-header(
@ -163,6 +165,36 @@ q-layout(view='hHh lpR fFf', container)
hide-bottom-space hide-bottom-space
:aria-label='t(`navEdit.label`)' :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-card.q-pa-md.q-mt-md.flex
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
@ -303,9 +335,39 @@ q-layout(view='hHh lpR fFf', container)
) )
template(v-if='state.current.type === `separator`') template(v-if='state.current.type === `separator`')
q-card q-card.q-pb-sm
q-card-section q-card-section
.text-subtitle1 {{t('navEdit.separator')}} .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-card.q-pa-md.q-mt-md.flex
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
@ -322,11 +384,12 @@ q-layout(view='hHh lpR fFf', container)
<script setup> <script setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { onMounted, reactive, ref } from 'vue' import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { cloneDeep } from 'lodash-es' import { cloneDeep, last, pick } from 'lodash-es'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { Sortable } from 'sortablejs-vue3' import { Sortable } from 'sortablejs-vue3'
@ -339,6 +402,7 @@ const $q = useQuasar()
// STORES // STORES
const pageStore = usePageStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
// I18N // I18N
@ -356,7 +420,7 @@ const state = reactive({
icon: '', icon: '',
target: '/', target: '/',
openInNewWindow: false, openInNewWindow: false,
visibility: [], visibilityGroups: [],
visibilityLimited: false, visibilityLimited: false,
isNested: false isNested: false
}, },
@ -396,7 +460,9 @@ function setItem (item) {
function addItem (type) { function addItem (type) {
const newItem = { const newItem = {
id: uuid(), id: uuid(),
type type,
visibilityGroups: [],
visibilityLimited: false
} }
switch (type) { switch (type) {
case 'header': { case 'header': {
@ -408,8 +474,6 @@ function addItem (type) {
newItem.icon = 'mdi-text-box-outline' newItem.icon = 'mdi-text-box-outline'
newItem.target = '/' newItem.target = '/'
newItem.openInNewWindow = false newItem.openInNewWindow = false
newItem.visibilityGroups = []
newItem.visibilityLimited = false
newItem.isNested = false newItem.isNested = false
break break
} }
@ -431,6 +495,11 @@ function clearItems () {
state.current = {} state.current = {}
} }
function updateItemPosition (ev) {
const item = state.items.splice(ev.oldIndex, 1)[0]
state.items.splice(ev.newIndex, 0, item)
}
function close () { function close () {
siteStore.$patch({ overlay: '' }) siteStore.$patch({ overlay: '' })
} }
@ -452,9 +521,163 @@ async function loadGroups () {
state.loading-- 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(() => { onMounted(() => {
loadMenuItems()
loadGroups() loadGroups()
}) })
onBeforeUnmount(() => {
siteStore.overlayOpts = {}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

@ -3,16 +3,34 @@ q-scroll-area.sidebar-nav(
:thumb-style='thumbStyle' :thumb-style='thumbStyle'
:bar-style='barStyle' :bar-style='barStyle'
) )
q-list( q-list.sidebar-nav-list(
clickable clickable
dense dense
dark dark
) )
template(v-for='item of siteStore.nav.items') template(v-for='item of siteStore.nav.items', :key='item.id')
q-item-label.text-blue-2.text-caption.text-wordbreak-all( q-item-label.text-blue-2.text-caption.text-wordbreak-all(
v-if='item.type === `header`' v-if='item.type === `header`'
header header
) {{ item.label }} ) {{ item.label }}
q-expansion-item(
v-else-if='item.type === `link` && item.children?.length > 0'
:icon='item.icon'
:label='item.label'
dense
)
q-list(
clickable
dense
dark
)
q-item(
v-for='itemChild of item.children'
:to='itemChild.target'
)
q-item-section(side)
q-icon(:name='itemChild.icon', color='white')
q-item-section.text-wordbreak-all.text-white {{ itemChild.label }}
q-item( q-item(
v-else-if='item.type === `link`' v-else-if='item.type === `link`'
:to='item.target' :to='item.target'
@ -20,7 +38,7 @@ q-scroll-area.sidebar-nav(
q-item-section(side) q-item-section(side)
q-icon(:name='item.icon', color='white') q-icon(:name='item.icon', color='white')
q-item-section.text-wordbreak-all.text-white {{ item.label }} q-item-section.text-wordbreak-all.text-white {{ item.label }}
q-separator.q-my-sm( q-separator(
v-else-if='item.type === `separator`' v-else-if='item.type === `separator`'
dark dark
) )
@ -71,7 +89,7 @@ const barStyle = {
// WATCHERS // WATCHERS
watch(() => pageStore.navigationId, (newValue) => { watch(() => pageStore.navigationId, (newValue) => {
if (newValue !== siteStore.nav.currentId) { if (newValue && newValue !== siteStore.nav.currentId) {
siteStore.fetchNavigation(newValue) siteStore.fetchNavigation(newValue)
} }
}, { immediate: true }) }, { immediate: true })
@ -83,9 +101,78 @@ watch(() => pageStore.navigationId, (newValue) => {
border-top: 1px solid rgba(255,255,255,.15); border-top: 1px solid rgba(255,255,255,.15);
height: calc(100% - 38px - 24px); height: calc(100% - 38px - 24px);
&-list > .q-separator {
margin-top: 10px;
margin-bottom: 10px;
}
.q-list { .q-list {
.q-separator + .q-item__label { .q-separator + .q-item__label {
padding-top: 12px; padding-top: 10px;
}
.q-item__section--avatar {
min-width: auto;
}
.q-expansion-item > .q-expansion-item__container {
> .q-item {
&::before {
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0px;
width: 10px;
height: 10px;
border-style: solid;
border-color: transparent transparent rgba(255,255,255,.25) rgba(255,255,255,.25);
transition: all .4s ease;
}
}
&::after {
content: '';
display: block;
position: absolute;
bottom: -20px;
left: 0;
width: 10px;
height: 10px;
border-style: solid;
border-color: rgba(255,255,255,.25) transparent transparent rgba(255,255,255,.25);
transition: all .4s ease;
}
}
.q-expansion-item--collapsed > .q-expansion-item__container {
> .q-item {
&::before {
border-width: 0 0 0 0;
}
}
&::after {
bottom: 0px;
border-width: 0 0 0 0;
}
}
.q-expansion-item--expanded > .q-expansion-item__container {
> .q-item {
&::before {
border-width: 0 10px 10px 0;
}
}
&::after {
bottom: -20px;
border-width: 10px 10px 10px 0;
}
}
.q-expansion-item__content {
border-left: 10px solid rgba(255,255,255,.25);
} }
} }
} }

@ -341,7 +341,7 @@ async function createPage () {
$q.dialog({ $q.dialog({
component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')), component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
componentProps: { componentProps: {
mode: 'createPage', mode: 'savePage',
folderPath: '', folderPath: '',
itemTitle: pageStore.title, itemTitle: pageStore.title,
itemFileName: pageStore.path itemFileName: pageStore.path

@ -15,38 +15,6 @@ q-menu(
q-item-section.items-center(avatar) q-item-section.items-center(avatar)
q-icon(color='grey', name='las la-envelope', size='sm') q-icon(color='grey', name='las la-envelope', size='sm')
q-item-section.q-pr-md Email q-item-section.q-pr-md Email
q-item(clickable, @click='openSocialPop(`https://www.facebook.com/sharer/sharer.php?u=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title) + `&description=` + encodeURIComponent(props.description))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-facebook', size='sm')
q-item-section.q-pr-md Facebook
q-item(clickable, @click='openSocialPop(`https://www.linkedin.com/shareArticle?mini=true&url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title) + `&summary=` + encodeURIComponent(props.description))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-linkedin', size='sm')
q-item-section.q-pr-md LinkedIn
q-item(clickable, @click='openSocialPop(`https://www.reddit.com/submit?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-reddit', size='sm')
q-item-section.q-pr-md Reddit
q-item(clickable, @click='openSocialPop(`https://t.me/share/url?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(props.title))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-telegram', size='sm')
q-item-section.q-pr-md Telegram
q-item(clickable, @click='openSocialPop(`https://twitter.com/intent/tweet?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(props.title))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-twitter', size='sm')
q-item-section.q-pr-md Twitter
q-item(clickable, :href='`viber://forward?text=` + encodeURIComponent(urlFormatted) + ` ` + encodeURIComponent(props.description)')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-viber', size='sm')
q-item-section.q-pr-md Viber
q-item(clickable, @click='openSocialPop(`http://service.weibo.com/share/share.php?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-weibo', size='sm')
q-item-section.q-pr-md Weibo
q-item(clickable, @click='openSocialPop(`https://api.whatsapp.com/send?text=` + encodeURIComponent(props.title) + `%0D%0A` + encodeURIComponent(urlFormatted))')
q-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-whatsapp', size='sm')
q-item-section.q-pr-md Whatsapp
</template> </template>
<script setup> <script setup>

@ -1,9 +1,15 @@
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card.page-save-dialog(style='width: 860px; max-width: 90vw;') q-card.page-save-dialog(style='width: 860px; max-width: 90vw;')
q-card-section.card-header q-card-section.card-header(v-if='props.mode === `savePage`')
q-icon(name='img:/_assets/icons/fluent-save-as.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-save-as.svg', left, size='sm')
span {{t('pageSaveDialog.title')}} span {{t('pageSaveDialog.title')}}
q-card-section.card-header(v-else-if='props.mode === `duplicatePage`')
q-icon(name='img:/_assets/icons/color-documents.svg', left, size='sm')
span {{t('pageDuplicateDialog.title')}}
q-card-section.card-header(v-else-if='props.mode === `renamePage`')
q-icon(name='img:/_assets/icons/fluent-rename.svg', left, size='sm')
span {{t('pageRenameDialog.title')}}
.row.page-save-dialog-browser .row.page-save-dialog-browser
.col-4 .col-4
q-scroll-area( q-scroll-area(
@ -30,8 +36,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
clickable clickable
active-class='active' active-class='active'
:active='item.id === state.currentFileId' :active='item.id === state.currentFileId'
@click.native='state.currentFileId = item.id' @click.native='selectItem(item)'
@dblclick.native='openItem(item)'
) )
q-item-section(side) q-item-section(side)
q-icon(:name='item.icon', size='sm') q-icon(:name='item.icon', size='sm')
@ -47,6 +52,8 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
label='Page Title' label='Page Title'
dense dense
outlined outlined
autofocus
@focus='state.currentFileId = null'
) )
q-item q-item
blueprint-icon(icon='file-submodule') blueprint-icon(icon='file-submodule')
@ -56,6 +63,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
label='Path Name' label='Path Name'
dense dense
outlined outlined
@focus='state.pathDirty = true; state.currentFileId = null'
) )
q-card-actions.card-actions.q-px-md q-card-actions.card-actions.q-px-md
q-btn.acrylic-btn( q-btn.acrylic-btn(
@ -112,10 +120,11 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup> <script setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed, onMounted, reactive } from 'vue' import { computed, onMounted, reactive, watch } from 'vue'
import { useDialogPluginComponent, useQuasar } from 'quasar' import { useDialogPluginComponent, useQuasar } from 'quasar'
import { cloneDeep, find, initial, last } from 'lodash-es' import { cloneDeep, find, initial, last } from 'lodash-es'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import slugify from 'slugify'
import fileTypes from '../helpers/fileTypes' import fileTypes from '../helpers/fileTypes'
@ -132,7 +141,7 @@ const props = defineProps({
mode: { mode: {
type: String, type: String,
required: false, required: false,
default: 'pageSave' default: 'savePage'
}, },
itemId: { itemId: {
type: String, type: String,
@ -187,7 +196,8 @@ const state = reactive({
fileList: [], fileList: [],
title: '', title: '',
path: '', path: '',
typesToFetch: [] typesToFetch: [],
pathDirty: false
}) })
const thumbStyle = { const thumbStyle = {
@ -228,6 +238,17 @@ const files = computed(() => {
}) })
}) })
// WATCHERS
watch(() => state.title, (newValue) => {
if (state.pathDirty && !state.path) {
state.pathDirty = false
}
if (!state.pathDirty) {
state.path = slugify(newValue, { lower: true, strict: true })
}
})
// METHODS // METHODS
async function save () { async function save () {
@ -247,6 +268,7 @@ async function treeLazyLoad (nodeId, isCurrent, { done, fail }) {
async function loadTree ({ parentId = null, parentPath = null, types, initLoad = false }) { async function loadTree ({ parentId = null, parentPath = null, types, initLoad = false }) {
try { try {
state.fileList = []
const resp = await APOLLO_CLIENT.query({ const resp = await APOLLO_CLIENT.query({
query: gql` query: gql`
query loadTree ( query loadTree (
@ -322,6 +344,18 @@ async function loadTree ({ parentId = null, parentPath = null, types, initLoad =
} }
break break
} }
case 'TreeItemPage': {
state.fileList.push({
id: item.id,
type: 'page',
title: item.title,
pageType: 'markdown',
updatedAt: '2022-11-24T18:27:00Z',
folderPath: item.folderPath,
fileName: item.fileName
})
break
}
} }
} }
if (newTreeRoots.length > 0) { if (newTreeRoots.length > 0) {
@ -346,6 +380,12 @@ function treeContextAction (nodeId, action) {
} }
} }
function selectItem (item) {
state.currentFileId = item.id
state.title = item.title
state.path = item.fileName
}
function newFolder (parentId) { function newFolder (parentId) {
$q.dialog({ $q.dialog({
component: FolderCreateDialog, component: FolderCreateDialog,

@ -114,7 +114,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-bunch-of-keys.svg') q-icon(name='img:/_assets/icons/fluent-bunch-of-keys.svg')
q-item-section {{ t('admin.login.title') }} q-item-section {{ t('admin.login.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/navigation`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:navigation`)') q-item(:to='`/_admin/` + adminStore.currentSiteId + `/navigation`', v-ripple, active-class='bg-primary text-white', disabled, v-if='flagsStore.experimental && (userStore.can(`manage:sites`) || userStore.can(`manage:navigation`))')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-tree-structure.svg') q-icon(name='img:/_assets/icons/fluent-tree-structure.svg')
q-item-section {{ t('admin.navigation.title') }} q-item-section {{ t('admin.navigation.title') }}

@ -4,9 +4,53 @@ q-layout(view='hHh Lpr lff')
q-drawer.bg-sidebar( q-drawer.bg-sidebar(
:modelValue='isSidebarShown' :modelValue='isSidebarShown'
:show-if-above='siteStore.theme.sidebarPosition !== `off`' :show-if-above='siteStore.theme.sidebarPosition !== `off`'
:width='255' :width='isSidebarMini ? 56 : 255'
:side='siteStore.theme.sidebarPosition === `right` ? `right` : `left`' :side='siteStore.theme.sidebarPosition === `right` ? `right` : `left`'
) )
.sidebar-mini.column.items-stretch(v-if='isSidebarMini')
q-btn.q-py-md(
flat
icon='las la-globe'
color='white'
aria-label='Switch Locale'
@click=''
)
locale-selector-menu(anchor='top right' self='top left')
q-tooltip(anchor='center right' self='center left') Switch Locale
q-btn.q-py-md(
flat
icon='las la-sitemap'
color='white'
aria-label='Browse'
)
q-tooltip(anchor='center right' self='center left') Browse
q-separator.q-my-sm(inset, dark)
q-btn.q-py-md(
flat
icon='las la-bookmark'
color='white'
aria-label='Bookmarks'
)
q-tooltip(anchor='center right' self='center left') Bookmarks
q-space
q-btn.q-py-xs(
flat
icon='las la-dharmachakra'
color='white'
aria-label='Edit Nav'
size='sm'
)
q-menu(
ref='navEditMenuMini'
anchor='top right'
self='bottom left'
)
nav-edit-menu(
:menu-hide-handler='navEditMenuMini.hide'
:update-position-handler='navEditMenuMini.updatePosition'
)
q-tooltip(anchor='center right' self='center left') Edit Nav
template(v-else)
.sidebar-actions.flex.items-stretch .sidebar-actions.flex.items-stretch
q-btn.q-px-sm.col( q-btn.q-px-sm.col(
flat flat
@ -18,7 +62,7 @@ q-layout(view='hHh Lpr lff')
:aria-label='commonStore.locale' :aria-label='commonStore.locale'
size='sm' size='sm'
) )
locale-selector-menu locale-selector-menu(:offset="[-5, 5]")
q-separator(vertical) q-separator(vertical)
q-btn.q-px-sm.col( q-btn.q-px-sm.col(
flat flat
@ -33,14 +77,6 @@ q-layout(view='hHh Lpr lff')
nav-sidebar nav-sidebar
q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated') q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
q-btn.col( q-btn.col(
v-if='isRoot'
icon='las la-dharmachakra'
label='Edit Nav'
flat
@click='siteStore.$patch({ overlay: `NavEdit` })'
)
q-btn.col(
v-else
icon='las la-dharmachakra' icon='las la-dharmachakra'
label='Edit Nav' label='Edit Nav'
flat flat
@ -51,13 +87,15 @@ q-layout(view='hHh Lpr lff')
self='bottom left' self='bottom left'
:offset='[0, 10]' :offset='[0, 10]'
) )
nav-edit-menu(:menu-hide-handler='navEditMenu.hide') nav-edit-menu(
:menu-hide-handler='navEditMenu.hide'
:update-position-handler='navEditMenu.updatePosition'
)
q-separator(vertical) q-separator(vertical)
q-btn.col( q-btn.col(
icon='las la-bookmark' icon='las la-bookmark'
label='Bookmarks' label='Bookmarks'
flat flat
disabled
) )
q-page-container q-page-container
router-view router-view
@ -129,6 +167,7 @@ useMeta({
// REFS // REFS
const navEditMenu = ref(null) const navEditMenu = ref(null)
const navEditMenuMini = ref(null)
// COMPUTED // COMPUTED
@ -136,8 +175,8 @@ const isSidebarShown = computed(() => {
return siteStore.showSideNav && !siteStore.sideNavIsDisabled && !(editorStore.isActive && editorStore.hideSideNav) return siteStore.showSideNav && !siteStore.sideNavIsDisabled && !(editorStore.isActive && editorStore.hideSideNav)
}) })
const isRoot = computed(() => { const isSidebarMini = computed(() => {
return pageStore.path === '' || pageStore.path === 'home' return ['hide', 'hideExact'].includes(pageStore.navigationMode) || !pageStore.navigationId
}) })
</script> </script>
@ -149,6 +188,10 @@ const isRoot = computed(() => {
height: 38px; height: 38px;
} }
.sidebar-mini {
height: 100%;
}
body.body--dark { body.body--dark {
background-color: $dark-6; background-color: $dark-6;
} }

@ -142,6 +142,20 @@ q-page.admin-general
q-card.q-pb-sm.q-mt-md q-card.q-pb-sm.q-mt-md
q-card-section q-card-section
.text-subtitle1 {{t('admin.general.features')}} .text-subtitle1 {{t('admin.general.features')}}
q-item(tag='label')
blueprint-icon(icon='tree-structure')
q-item-section
q-item-label {{t(`admin.general.allowBrowse`)}}
q-item-label(caption) {{t(`admin.general.allowBrowseHint`)}}
q-item-section(avatar)
q-toggle(
v-model='state.config.features.browse'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.general.allowBrowse`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label') q-item(tag='label')
blueprint-icon(icon='discussion-forum') blueprint-icon(icon='discussion-forum')
q-item-section q-item-section
@ -631,6 +645,7 @@ async function load () {
follow follow
} }
features { features {
browse
comments comments
contributions contributions
profile profile
@ -702,6 +717,7 @@ async function save () {
follow: state.config.robots?.follow ?? false follow: state.config.robots?.follow ?? false
}, },
features: { features: {
browse: state.config.features?.browse ?? false,
comments: state.config.features?.comments ?? false, comments: state.config.features?.comments ?? false,
ratings: (state.config.features?.ratings || 'off') !== 'off', ratings: (state.config.features?.ratings || 'off') !== 'off',
ratingsMode: state.config.features?.ratingsMode ?? 'off', ratingsMode: state.config.features?.ratingsMode ?? 'off',

@ -22,6 +22,7 @@ const pagePropsFragment = gql`
isSearchable isSearchable
locale locale
navigationId navigationId
navigationMode
password password
path path
publishEndDate publishEndDate
@ -199,6 +200,7 @@ export const usePageStore = defineStore('page', {
isSearchable: true, isSearchable: true,
locale: 'en', locale: 'en',
navigationId: null, navigationId: null,
navigationMode: 'inherit',
password: '', password: '',
path: '', path: '',
publishEndDate: '', publishEndDate: '',
@ -237,6 +239,9 @@ export const usePageStore = defineStore('page', {
}, },
folderPath: (state) => { folderPath: (state) => {
return initial(state.path.split('/')).join('/') return initial(state.path.split('/')).join('/')
},
isHome: (state) => {
return ['', 'home'].includes(state.path)
} }
}, },
actions: { actions: {

@ -133,6 +133,7 @@ export const useSiteStore = defineStore('site', {
} }
} }
features { features {
browse
profile profile
ratingsMode ratingsMode
reasonForChange reasonForChange

Loading…
Cancel
Save