Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
607b8d81
Unverified
Commit
607b8d81
authored
Jul 25, 2023
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: edit navigation
parent
5eaba2cb
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
835 additions
and
170 deletions
+835
-170
3.0.0.mjs
server/db/migrations/3.0.0.mjs
+10
-10
navigation.mjs
server/graph/resolvers/navigation.mjs
+108
-4
navigation.graphql
server/graph/schemas/navigation.graphql
+37
-5
page.graphql
server/graph/schemas/page.graphql
+1
-0
site.graphql
server/graph/schemas/site.graphql
+2
-0
en.json
server/locales/en.json
+6
-0
navigation.mjs
server/models/navigation.mjs
+1
-0
pageHistory.mjs
server/models/pageHistory.mjs
+0
-8
pages.mjs
server/models/pages.mjs
+2
-1
sites.mjs
server/models/sites.mjs
+9
-13
tree.mjs
server/models/tree.mjs
+6
-3
color-documents.svg
ux/public/_assets/icons/color-documents.svg
+2
-0
ultraviolet-duplicate.svg
ux/public/_assets/icons/ultraviolet-duplicate.svg
+2
-0
ultraviolet-tree-structure.svg
ux/public/_assets/icons/ultraviolet-tree-structure.svg
+2
-0
LocaleSelectorMenu.vue
ux/src/components/LocaleSelectorMenu.vue
+20
-2
NavEditMenu.vue
ux/src/components/NavEditMenu.vue
+151
-31
NavEditOverlay.vue
ux/src/components/NavEditOverlay.vue
+230
-7
NavSidebar.vue
ux/src/components/NavSidebar.vue
+92
-5
PageHeader.vue
ux/src/components/PageHeader.vue
+1
-1
SocialSharingMenu.vue
ux/src/components/SocialSharingMenu.vue
+0
-32
TreeBrowserDialog.vue
ux/src/components/TreeBrowserDialog.vue
+46
-6
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+1
-1
MainLayout.vue
ux/src/layouts/MainLayout.vue
+84
-41
AdminGeneral.vue
ux/src/pages/AdminGeneral.vue
+16
-0
page.js
ux/src/stores/page.js
+5
-0
site.js
ux/src/stores/site.js
+1
-0
No files found.
server/db/migrations/3.0.0.mjs
View file @
607b8d81
...
...
@@ -179,7 +179,6 @@ export async function up (knex) {
// NAVIGATION ----------------------------
.
createTable
(
'navigation'
,
table
=>
{
table
.
uuid
(
'id'
).
notNullable
().
primary
().
defaultTo
(
knex
.
raw
(
'gen_random_uuid()'
))
table
.
string
(
'name'
).
notNullable
()
table
.
jsonb
(
'items'
).
notNullable
().
defaultTo
(
'[]'
)
})
// PAGE HISTORY ------------------------
...
...
@@ -298,6 +297,8 @@ export async function up (knex) {
table
.
enu
(
'type'
,
[
'folder'
,
'page'
,
'asset'
]).
notNullable
().
index
()
table
.
string
(
'locale'
,
10
).
notNullable
().
defaultTo
(
'en'
).
index
()
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
.
timestamp
(
'createdAt'
).
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
(
'tree'
,
table
=>
{
table
.
uuid
(
'navigationId'
).
references
(
'id'
).
inTable
(
'navigation'
).
index
()
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
})
.
table
(
'userKeys'
,
table
=>
{
...
...
@@ -415,7 +415,6 @@ export async function up (knex) {
const
groupAdminId
=
uuid
()
const
groupGuestId
=
'10000000-0000-4000-8000-000000000001'
const
navDefaultId
=
uuid
()
const
siteId
=
uuid
()
const
authModuleId
=
uuid
()
const
userAdminId
=
uuid
()
...
...
@@ -568,6 +567,7 @@ export async function up (knex) {
}
},
features
:
{
browse
:
true
,
ratings
:
false
,
ratingsMode
:
'off'
,
comments
:
false
,
...
...
@@ -622,10 +622,6 @@ export async function up (knex) {
config
:
{}
}
},
nav
:
{
mode
:
'mixed'
,
defaultId
:
navDefaultId
,
},
theme
:
{
dark
:
false
,
codeBlocksTheme
:
'github-dark'
,
...
...
@@ -757,13 +753,13 @@ export async function up (knex) {
// -> NAVIGATION
await
knex
(
'navigation'
).
insert
({
id
:
navDefaultId
,
name
:
'Default'
,
id
:
siteId
,
items
:
JSON
.
stringify
([
{
id
:
uuid
(),
type
:
'header'
,
label
:
'Sample Header'
label
:
'Sample Header'
,
visibilityGroups
:
[]
},
{
id
:
uuid
(),
...
...
@@ -772,6 +768,7 @@ export async function up (knex) {
label
:
'Sample Link 1'
,
target
:
'/'
,
openInNewWindow
:
false
,
visibilityGroups
:
[],
children
:
[]
},
{
...
...
@@ -781,11 +778,13 @@ export async function up (knex) {
label
:
'Sample Link 2'
,
target
:
'/'
,
openInNewWindow
:
false
,
visibilityGroups
:
[],
children
:
[]
},
{
id
:
uuid
(),
type
:
'separator'
,
visibilityGroups
:
[]
},
{
id
:
uuid
(),
...
...
@@ -794,6 +793,7 @@ export async function up (knex) {
label
:
'Sample Link 3'
,
target
:
'/'
,
openInNewWindow
:
false
,
visibilityGroups
:
[],
children
:
[]
}
]),
...
...
server/graph/resolvers/navigation.mjs
View file @
607b8d81
import
{
generateError
,
generateSuccess
}
from
'../../helpers/graph.mjs'
import
{
isNil
}
from
'lodash-es'
export
default
{
Query
:
{
...
...
@@ -9,15 +10,118 @@ export default {
Mutation
:
{
async
updateNavigation
(
obj
,
args
,
context
)
{
try
{
// await WIKI.db.navigation.query().patch({
// config: args.tree
// }).where('key', 'site')
let
updateInherited
=
false
let
updateInheritedNavId
=
null
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) {
// await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)
// }
return
{
responseResult
:
generateSuccess
(
'Navigation updated successfully'
)
operation
:
generateSuccess
(
'Navigation updated successfully'
),
navigationMode
:
args
.
mode
,
navigationId
:
updateNavId
}
}
catch
(
err
)
{
return
generateError
(
err
)
...
...
server/graph/schemas/navigation.graphql
View file @
607b8d81
...
...
@@ -14,10 +14,10 @@ extend type Query {
extend
type
Mutation
{
updateNavigation
(
i
d
:
UUID
!
name
:
String
!
items
:
[
JSON
]!
):
Default
Response
pageI
d
:
UUID
!
mode
:
NavigationMode
!
items
:
[
NavigationItemInput
!]
):
NavigationUpdate
Response
}
# -----------------------------------------------
...
...
@@ -26,10 +26,42 @@ extend type Mutation {
type
NavigationItem
{
id
:
UUID
type
:
String
type
:
NavigationItemType
label
:
String
icon
:
String
target
:
String
openInNewWindow
:
Boolean
visibilityGroups
:
[
UUID
]
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
}
server/graph/schemas/page.graphql
View file @
607b8d81
...
...
@@ -238,6 +238,7 @@ type Page {
isSearchable
:
Boolean
locale
:
String
navigationId
:
UUID
navigationMode
:
NavigationMode
password
:
String
path
:
String
publishEndDate
:
Date
...
...
server/graph/schemas/site.graphql
View file @
607b8d81
...
...
@@ -79,6 +79,7 @@ type SiteRobots {
}
type
SiteFeatures
{
browse
:
Boolean
ratings
:
Boolean
ratingsMode
:
SitePageRatingMode
comments
:
Boolean
...
...
@@ -194,6 +195,7 @@ input SiteRobotsInput {
}
input
SiteFeaturesInput
{
browse
:
Boolean
ratings
:
Boolean
ratingsMode
:
SitePageRatingMode
comments
:
Boolean
...
...
server/locales/en.json
View file @
607b8d81
...
...
@@ -204,6 +204,8 @@
"admin.flags.title"
:
"Flags"
,
"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.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.allowCommentsHint"
:
"Can users leave comments on pages? Can be restricted using Page Rules."
,
"admin.general.allowContributions"
:
"Allow Contributions"
,
...
...
@@ -1697,6 +1699,8 @@
"navEdit.noSelection"
:
"Select a menu item from the left to start editing."
,
"navEdit.openInNewWindow"
:
"Open in New Window"
,
"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.separator"
:
"Separator"
,
"navEdit.target"
:
"Target"
,
...
...
@@ -1711,6 +1715,8 @@
"pageDeleteDialog.deleteSuccess"
:
"Page deleted successfully."
,
"pageDeleteDialog.pageId"
:
"Page ID {id}"
,
"pageDeleteDialog.title"
:
"Confirm Page Deletion"
,
"pageDuplicateDialog.title"
:
"Duplicate and Save As..."
,
"pageRenameDialog.title"
:
"Rename / Move to..."
,
"pageSaveDialog.displayModePath"
:
"Browse Using Paths"
,
"pageSaveDialog.displayModeTitle"
:
"Browse Using Titles"
,
"pageSaveDialog.title"
:
"Save As..."
,
...
...
server/models/navigation.mjs
View file @
607b8d81
...
...
@@ -20,6 +20,7 @@ export class Navigation extends Model {
static
async
getNav
({
id
,
cache
=
false
,
userGroups
=
[]
})
{
const
result
=
await
WIKI
.
db
.
navigation
.
query
().
findById
(
id
).
select
(
'items'
)
if
(
!
result
)
{
return
[]
}
return
result
.
items
.
filter
(
item
=>
{
return
!
item
.
visibilityGroups
?.
length
||
intersection
(
item
.
visibilityGroups
,
userGroups
).
length
>
0
}).
map
(
item
=>
{
...
...
server/models/pageHistory.mjs
View file @
607b8d81
...
...
@@ -64,14 +64,6 @@ export class PageHistory extends Model {
from
:
'pageHistory.authorId'
,
to
:
'users.id'
}
},
locale
:
{
relation
:
Model
.
BelongsToOneRelation
,
modelClass
:
Locale
,
join
:
{
from
:
'pageHistory.locale'
,
to
:
'locales.code'
}
}
}
}
...
...
server/models/pages.mjs
View file @
607b8d81
...
...
@@ -1220,7 +1220,8 @@ export class Page extends Model {
creatorName
:
'creator.name'
,
creatorEmail
:
'creator.email'
},
'tree.navigationId'
'tree.navigationId'
,
'tree.navigationMode'
])
.
joinRelated
(
'author'
)
.
joinRelated
(
'creator'
)
...
...
server/models/sites.mjs
View file @
607b8d81
...
...
@@ -48,16 +48,7 @@ export class Site extends Model {
}
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
({
id
:
newSiteId
,
hostname
,
isEnabled
:
true
,
config
:
defaultsDeep
(
config
,
{
...
...
@@ -76,6 +67,7 @@ export class Site extends Model {
}
},
features
:
{
browse
:
true
,
ratings
:
false
,
ratingsMode
:
'off'
,
comments
:
false
,
...
...
@@ -148,10 +140,6 @@ export class Site extends Model {
config
:
{}
}
},
nav
:
{
mode
:
'mixed'
,
defaultId
:
newDefaultNav
.
id
,
},
uploads
:
{
conflictBehavior
:
'overwrite'
,
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
}
`
)
await
WIKI
.
db
.
storage
.
query
().
insert
({
...
...
server/models/tree.mjs
View file @
607b8d81
...
...
@@ -147,7 +147,8 @@ export class Tree extends Model {
hash
:
generateHash
(
fullPath
),
locale
:
locale
,
siteId
,
meta
meta
,
navigationId
:
siteId
,
}).
returning
(
'*'
)
return
pageEntry
[
0
]
...
...
@@ -245,7 +246,8 @@ export class Tree extends Model {
siteId
:
siteId
,
locale
:
locale
,
folderPath
:
encodeFolderPath
(
parentPath
),
fileName
:
pathName
fileName
:
pathName
,
type
:
'folder'
}).
first
()
if
(
existingFolder
)
{
throw
new
Error
(
'ERR_FOLDER_ALREADY_EXISTS'
)
...
...
@@ -354,7 +356,8 @@ export class Tree extends Model {
.
andWhere
({
siteId
:
folder
.
siteId
,
folderPath
:
folder
.
folderPath
,
fileName
:
pathName
fileName
:
pathName
,
type
:
'folder'
}).
first
()
if
(
existingFolder
)
{
throw
new
Error
(
'ERR_FOLDER_ALREADY_EXISTS'
)
...
...
ux/public/_assets/icons/color-documents.svg
0 → 100644
View file @
607b8d81
<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>
\ No newline at end of file
ux/public/_assets/icons/ultraviolet-duplicate.svg
0 → 100644
View file @
607b8d81
<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>
\ No newline at end of file
ux/public/_assets/icons/ultraviolet-tree-structure.svg
0 → 100644
View file @
607b8d81
<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>
\ No newline at end of file
ux/src/components/LocaleSelectorMenu.vue
View file @
607b8d81
<
template
lang=
"pug"
>
q-menu.translucent-menu(
auto-close
anchor='bottom left'
self='top left'
:anchor='props.anchor'
:self='props.self'
:offset='props.offset'
)
q-list(padding, style='min-width: 200px;')
q-item(
...
...
@@ -25,6 +26,23 @@ import { useQuasar } from 'quasar'
import
{
useCommonStore
}
from
'src/stores/common'
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
const
$q
=
useQuasar
()
...
...
ux/src/components/NavEditMenu.vue
View file @
607b8d81
...
...
@@ -3,35 +3,63 @@ 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', :disable='isRoot')
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', :disable='isRoot')
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', :disable='isRoot')
q-item-section
q-item-label Override Current Only
q-item-label(caption) Set menu items and settings only for this path.
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-section(side)
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-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='override')
q-item-section
q-item-label Override Current + Descendants
q-item-label(caption) Set menu items and settings for this path and all descendants.
q-item(tag='label')
q-item-section(side)
q-radio(v-model='state.mode', val='overrideExact')
q-item-section
q-item-label Override Current Only
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-space
q-btn.acrylic-btn(
...
...
@@ -46,12 +74,16 @@ q-card(style='min-width: 350px')
:label='t(`common.actions.save`)'
color='positive'
padding='xs md'
@click='save'
:loading='state.loading > 0'
)
</
template
>
<
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
{
useQuasar
}
from
'quasar'
import
gql
from
'graphql-tag'
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
...
...
@@ -62,9 +94,17 @@ const props = defineProps({
menuHideHandler
:
{
type
:
Function
,
default
:
()
=>
({})
},
updatePositionHandler
:
{
type
:
Function
,
default
:
()
=>
({})
}
})
// QUASAR
const
$q
=
useQuasar
()
// STORES
const
pageStore
=
usePageStore
()
...
...
@@ -77,14 +117,94 @@ const { t } = useI18n()
// DATA
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
function
startEditing
()
{
siteStore
.
$patch
({
overlay
:
'NavEdit'
})
siteStore
.
$patch
({
overlay
:
'NavEdit'
,
overlayOpts
:
{
mode
:
state
.
mode
}
})
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
>
ux/src/components/NavEditOverlay.vue
View file @
607b8d81
...
...
@@ -38,6 +38,7 @@ q-layout(view='hHh lpR fFf', container)
: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)
...
...
@@ -50,6 +51,7 @@ q-layout(view='hHh lpR fFf', container)
:list='state.items'
item-key='id'
:options='sortableOptions'
@end='updateItemPosition'
)
template(#item='{element}')
.nav-edit-item.nav-edit-item-header(
...
...
@@ -163,6 +165,36 @@ q-layout(view='hHh lpR fFf', container)
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(
...
...
@@ -303,9 +335,39 @@ q-layout(view='hHh lpR fFf', container)
)
template(v-if='state.current.type === `separator`')
q-card
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(
...
...
@@ -322,11 +384,12 @@ q-layout(view='hHh lpR fFf', container)
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
useQuasar
}
from
'quasar'
import
{
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
on
BeforeUnmount
,
on
Mounted
,
reactive
,
ref
}
from
'vue'
import
{
v4
as
uuid
}
from
'uuid'
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
{
Sortable
}
from
'sortablejs-vue3'
...
...
@@ -339,6 +402,7 @@ const $q = useQuasar()
// STORES
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
// I18N
...
...
@@ -356,7 +420,7 @@ const state = reactive({
icon
:
''
,
target
:
'/'
,
openInNewWindow
:
false
,
visibility
:
[],
visibility
Groups
:
[],
visibilityLimited
:
false
,
isNested
:
false
},
...
...
@@ -396,7 +460,9 @@ function setItem (item) {
function
addItem
(
type
)
{
const
newItem
=
{
id
:
uuid
(),
type
type
,
visibilityGroups
:
[],
visibilityLimited
:
false
}
switch
(
type
)
{
case
'header'
:
{
...
...
@@ -408,8 +474,6 @@ function addItem (type) {
newItem
.
icon
=
'mdi-text-box-outline'
newItem
.
target
=
'/'
newItem
.
openInNewWindow
=
false
newItem
.
visibilityGroups
=
[]
newItem
.
visibilityLimited
=
false
newItem
.
isNested
=
false
break
}
...
...
@@ -431,6 +495,11 @@ function clearItems () {
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
:
''
})
}
...
...
@@ -452,9 +521,163 @@ async function loadGroups () {
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
>
...
...
ux/src/components/NavSidebar.vue
View file @
607b8d81
...
...
@@ -3,16 +3,34 @@ q-scroll-area.sidebar-nav(
:thumb-style='thumbStyle'
:bar-style='barStyle'
)
q-list(
q-list
.sidebar-nav-list
(
clickable
dense
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(
v-if='item.type === `header`'
header
)
{{
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(
v-else-if='item.type === `link`'
:to='item.target'
...
...
@@ -20,7 +38,7 @@ q-scroll-area.sidebar-nav(
q-item-section(side)
q-icon(:name='item.icon', color='white')
q-item-section.text-wordbreak-all.text-white
{{
item
.
label
}}
q-separator
.q-my-sm
(
q-separator(
v-else-if='item.type === `separator`'
dark
)
...
...
@@ -71,7 +89,7 @@ const barStyle = {
// WATCHERS
watch
(()
=>
pageStore
.
navigationId
,
(
newValue
)
=>
{
if
(
newValue
!==
siteStore
.
nav
.
currentId
)
{
if
(
newValue
&&
newValue
!==
siteStore
.
nav
.
currentId
)
{
siteStore
.
fetchNavigation
(
newValue
)
}
},
{
immediate
:
true
})
...
...
@@ -83,9 +101,78 @@ watch(() => pageStore.navigationId, (newValue) => {
border-top
:
1px
solid
rgba
(
255
,
255
,
255
,.
15
);
height
:
calc
(
100%
-
38px
-
24px
);
&
-list
>
.q-separator
{
margin-top
:
10px
;
margin-bottom
:
10px
;
}
.q-list
{
.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
);
}
}
}
...
...
ux/src/components/PageHeader.vue
View file @
607b8d81
...
...
@@ -341,7 +341,7 @@ async function createPage () {
$q
.
dialog
({
component
:
defineAsyncComponent
(()
=>
import
(
'../components/TreeBrowserDialog.vue'
)),
componentProps
:
{
mode
:
'
creat
ePage'
,
mode
:
'
sav
ePage'
,
folderPath
:
''
,
itemTitle
:
pageStore
.
title
,
itemFileName
:
pageStore
.
path
...
...
ux/src/components/SocialSharingMenu.vue
View file @
607b8d81
...
...
@@ -15,38 +15,6 @@ q-menu(
q-item-section.items-center(avatar)
q-icon(color='grey', name='las la-envelope', size='sm')
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
>
<
script
setup
>
...
...
ux/src/components/TreeBrowserDialog.vue
View file @
607b8d81
<
template
lang=
"pug"
>
q-dialog(ref='dialogRef', @hide='onDialogHide')
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')
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
.col-4
q-scroll-area(
...
...
@@ -30,8 +36,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
clickable
active-class='active'
:active='item.id === state.currentFileId'
@click.native='state.currentFileId = item.id'
@dblclick.native='openItem(item)'
@click.native='selectItem(item)'
)
q-item-section(side)
q-icon(:name='item.icon', size='sm')
...
...
@@ -47,6 +52,8 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
label='Page Title'
dense
outlined
autofocus
@focus='state.currentFileId = null'
)
q-item
blueprint-icon(icon='file-submodule')
...
...
@@ -56,6 +63,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
label='Path Name'
dense
outlined
@focus='state.pathDirty = true; state.currentFileId = null'
)
q-card-actions.card-actions.q-px-md
q-btn.acrylic-btn(
...
...
@@ -112,10 +120,11 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
computed
,
onMounted
,
reactive
}
from
'vue'
import
{
computed
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
cloneDeep
,
find
,
initial
,
last
}
from
'lodash-es'
import
gql
from
'graphql-tag'
import
slugify
from
'slugify'
import
fileTypes
from
'../helpers/fileTypes'
...
...
@@ -132,7 +141,7 @@ const props = defineProps({
mode
:
{
type
:
String
,
required
:
false
,
default
:
'
pageSav
e'
default
:
'
savePag
e'
},
itemId
:
{
type
:
String
,
...
...
@@ -187,7 +196,8 @@ const state = reactive({
fileList
:
[],
title
:
''
,
path
:
''
,
typesToFetch
:
[]
typesToFetch
:
[],
pathDirty
:
false
})
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
async
function
save
()
{
...
...
@@ -247,6 +268,7 @@ async function treeLazyLoad (nodeId, isCurrent, { done, fail }) {
async
function
loadTree
({
parentId
=
null
,
parentPath
=
null
,
types
,
initLoad
=
false
})
{
try
{
state
.
fileList
=
[]
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query loadTree (
...
...
@@ -322,6 +344,18 @@ async function loadTree ({ parentId = null, parentPath = null, types, initLoad =
}
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
)
{
...
...
@@ -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
)
{
$q
.
dialog
({
component
:
FolderCreateDialog
,
...
...
ux/src/layouts/AdminLayout.vue
View file @
607b8d81
...
...
@@ -114,7 +114,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-bunch-of-keys.svg')
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-icon(name='img:/_assets/icons/fluent-tree-structure.svg')
q-item-section
{{
t
(
'admin.navigation.title'
)
}}
...
...
ux/src/layouts/MainLayout.vue
View file @
607b8d81
...
...
@@ -4,61 +4,99 @@ q-layout(view='hHh Lpr lff')
q-drawer.bg-sidebar(
:modelValue='isSidebarShown'
:show-if-above='siteStore.theme.sidebarPosition !== `off`'
:width='255'
:width='
isSidebarMini ? 56 :
255'
:side='siteStore.theme.sidebarPosition === `right` ? `right` : `left`'
)
.sidebar-
actions.flex.items-stretch
q-btn.q-p
x-sm.col
(
.sidebar-
mini.column.items-stretch(v-if='isSidebarMini')
q-btn.q-p
y-md
(
flat
dense
icon='las la-globe'
color='blue-7'
text-color='blue-2'
:label='commonStore.locale'
:aria-label='commonStore.locale'
size='sm'
color='white'
aria-label='Switch Locale'
@click=''
)
locale-selector-menu
q-separator(vertical)
q-btn.q-p
x-sm.col
(
locale-selector-menu
(anchor='top right' self='top left')
q-tooltip(anchor='center right' self='center left') Switch Locale
q-btn.q-p
y-md
(
flat
dense
icon='las la-sitemap'
color='blue-7'
text-color='blue-2'
label='Browse'
color='white'
aria-label='Browse'
size='sm'
)
nav-sidebar
q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
q-btn.col(
v-if='isRoot'
icon='las la-dharmachakra'
label='Edit Nav'
q-tooltip(anchor='center right' self='center left') Browse
q-separator.q-my-sm(inset, dark)
q-btn.q-py-md(
flat
@click='siteStore.$patch({ overlay: `NavEdit` })'
icon='las la-bookmark'
color='white'
aria-label='Bookmarks'
)
q-btn.col(
v-else
icon='las la-dharmachakra'
label='Edit Nav'
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='navEditMenu'
anchor='top
lef
t'
ref='navEditMenu
Mini
'
anchor='top
righ
t'
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
)
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
q-btn.q-px-sm.col(
flat
dense
icon='las la-globe'
color='blue-7'
text-color='blue-2'
:label='commonStore.locale'
:aria-label='commonStore.locale'
size='sm'
)
locale-selector-menu(:offset="[-5, 5]")
q-separator(vertical)
q-btn.q-px-sm.col(
flat
dense
icon='las la-sitemap'
color='blue-7'
text-color='blue-2'
label='Browse'
aria-label='Browse'
size='sm'
)
nav-sidebar
q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
q-btn.col(
icon='las la-dharmachakra'
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'
:update-position-handler='navEditMenu.updatePosition'
)
q-separator(vertical)
q-btn.col(
icon='las la-bookmark'
label='Bookmarks'
flat
)
q-page-container
router-view
q-page-scroller(
...
...
@@ -129,6 +167,7 @@ useMeta({
// REFS
const
navEditMenu
=
ref
(
null
)
const
navEditMenuMini
=
ref
(
null
)
// COMPUTED
...
...
@@ -136,8 +175,8 @@ const isSidebarShown = computed(() => {
return
siteStore
.
showSideNav
&&
!
siteStore
.
sideNavIsDisabled
&&
!
(
editorStore
.
isActive
&&
editorStore
.
hideSideNav
)
})
const
is
Root
=
computed
(()
=>
{
return
pageStore
.
path
===
''
||
pageStore
.
path
===
'home'
const
is
SidebarMini
=
computed
(()
=>
{
return
[
'hide'
,
'hideExact'
].
includes
(
pageStore
.
navigationMode
)
||
!
pageStore
.
navigationId
})
</
script
>
...
...
@@ -149,6 +188,10 @@ const isRoot = computed(() => {
height
:
38px
;
}
.sidebar-mini
{
height
:
100%
;
}
body
.body--dark
{
background-color
:
$dark-6
;
}
...
...
ux/src/pages/AdminGeneral.vue
View file @
607b8d81
...
...
@@ -143,6 +143,20 @@ q-page.admin-general
q-card-section
.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')
blueprint-icon(icon='discussion-forum')
q-item-section
q-item-label
{{
t
(
`admin.general.allowComments`
)
}}
...
...
@@ -631,6 +645,7 @@ async function load () {
follow
}
features {
browse
comments
contributions
profile
...
...
@@ -702,6 +717,7 @@ async function save () {
follow
:
state
.
config
.
robots
?.
follow
??
false
},
features
:
{
browse
:
state
.
config
.
features
?.
browse
??
false
,
comments
:
state
.
config
.
features
?.
comments
??
false
,
ratings
:
(
state
.
config
.
features
?.
ratings
||
'off'
)
!==
'off'
,
ratingsMode
:
state
.
config
.
features
?.
ratingsMode
??
'off'
,
...
...
ux/src/stores/page.js
View file @
607b8d81
...
...
@@ -22,6 +22,7 @@ const pagePropsFragment = gql`
isSearchable
locale
navigationId
navigationMode
password
path
publishEndDate
...
...
@@ -199,6 +200,7 @@ export const usePageStore = defineStore('page', {
isSearchable
:
true
,
locale
:
'en'
,
navigationId
:
null
,
navigationMode
:
'inherit'
,
password
:
''
,
path
:
''
,
publishEndDate
:
''
,
...
...
@@ -237,6 +239,9 @@ export const usePageStore = defineStore('page', {
},
folderPath
:
(
state
)
=>
{
return
initial
(
state
.
path
.
split
(
'/'
)).
join
(
'/'
)
},
isHome
:
(
state
)
=>
{
return
[
''
,
'home'
].
includes
(
state
.
path
)
}
},
actions
:
{
...
...
ux/src/stores/site.js
View file @
607b8d81
...
...
@@ -133,6 +133,7 @@ export const useSiteStore = defineStore('site', {
}
}
features {
browse
profile
ratingsMode
reasonForChange
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment