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
c377eca6
Unverified
Commit
c377eca6
authored
Jan 01, 2023
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: rename folder + fileman improvements
parent
bfbb64a7
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
324 additions
and
6 deletions
+324
-6
tree.js
server/graph/resolvers/tree.js
+86
-2
tree.graphql
server/graph/schemas/tree.graphql
+5
-2
fluent-rename.svg
ux/public/_assets/icons/fluent-rename.svg
+2
-0
FileManager.vue
ux/src/components/FileManager.vue
+0
-0
FolderRenameDialog.vue
ux/src/components/FolderRenameDialog.vue
+226
-0
PageSaveDialog.vue
ux/src/components/PageSaveDialog.vue
+1
-1
en.json
ux/src/i18n/locales/en.json
+4
-1
No files found.
server/graph/resolvers/tree.js
View file @
c377eca6
...
...
@@ -88,6 +88,18 @@ module.exports = {
childrenCount
:
0
}
}))
},
async
folderById
(
obj
,
args
,
context
)
{
const
folder
=
await
WIKI
.
db
.
knex
(
'tree'
)
.
select
(
WIKI
.
db
.
knex
.
raw
(
'tree.*, nlevel(tree."folderPath") AS depth'
))
.
where
(
'id'
,
args
.
id
)
.
first
()
return
{
...
folder
,
folderPath
:
folder
.
folderPath
.
replaceAll
(
'.'
,
'/'
).
replaceAll
(
'_'
,
'-'
),
childrenCount
:
0
}
}
},
Mutation
:
{
...
...
@@ -96,6 +108,8 @@ module.exports = {
*/
async
createFolder
(
obj
,
args
,
context
)
{
try
{
WIKI
.
logger
.
debug
(
`Creating new folder
${
args
.
pathName
}
...`
)
// Get parent path
let
parentPath
=
''
if
(
args
.
parentId
)
{
...
...
@@ -128,6 +142,7 @@ module.exports = {
}
// Create folder
WIKI
.
logger
.
debug
(
`Creating new folder
${
args
.
pathName
}
at path /
${
parentPath
}
...`
)
await
WIKI
.
db
.
knex
(
'tree'
).
insert
({
folderPath
:
parentPath
,
fileName
:
args
.
pathName
,
...
...
@@ -139,6 +154,74 @@ module.exports = {
operation
:
graphHelper
.
generateSuccess
(
'Folder created successfully'
)
}
}
catch
(
err
)
{
WIKI
.
logger
.
debug
(
`Failed to create folder:
${
err
.
message
}
`
)
return
graphHelper
.
generateError
(
err
)
}
},
/**
* RENAME FOLDER
*/
async
renameFolder
(
obj
,
args
,
context
)
{
try
{
// Get folder
const
folder
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
args
.
folderId
).
first
()
WIKI
.
logger
.
debug
(
`Renaming folder
${
folder
.
id
}
path to
${
args
.
pathName
}
...`
)
// Validate path name
if
(
!
rePathName
.
test
(
args
.
pathName
))
{
throw
new
Error
(
'ERR_INVALID_PATH_NAME'
)
}
// Validate title
if
(
!
reTitle
.
test
(
args
.
title
))
{
throw
new
Error
(
'ERR_INVALID_TITLE'
)
}
if
(
args
.
pathName
!==
folder
.
fileName
)
{
// Check for collision
const
existingFolder
=
await
WIKI
.
db
.
knex
(
'tree'
)
.
whereNot
(
'id'
,
folder
.
id
)
.
andWhere
({
siteId
:
folder
.
siteId
,
folderPath
:
folder
.
folderPath
,
fileName
:
args
.
pathName
}).
first
()
if
(
existingFolder
)
{
throw
new
Error
(
'ERR_FOLDER_ALREADY_EXISTS'
)
}
// Build new paths
const
oldFolderPath
=
(
folder
.
folderPath
?
`
${
folder
.
folderPath
}
.
${
folder
.
fileName
}
`
:
folder
.
fileName
).
replaceAll
(
'-'
,
'_'
)
const
newFolderPath
=
(
folder
.
folderPath
?
`
${
folder
.
folderPath
}
.
${
args
.
pathName
}
`
:
args
.
pathName
).
replaceAll
(
'-'
,
'_'
)
// Update children nodes
WIKI
.
logger
.
debug
(
`Updating parent path of children nodes from
${
oldFolderPath
}
to
${
newFolderPath
}
...`
)
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'siteId'
,
folder
.
siteId
).
andWhere
(
'folderPath'
,
oldFolderPath
).
update
({
folderPath
:
newFolderPath
})
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'siteId'
,
folder
.
siteId
).
andWhere
(
'folderPath'
,
'<@'
,
oldFolderPath
).
update
({
folderPath
:
WIKI
.
db
.
knex
.
raw
(
`'
${
newFolderPath
}
' || subpath(tree."folderPath", nlevel('
${
newFolderPath
}
'))`
)
})
// Rename the folder itself
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
folder
.
id
).
update
({
fileName
:
args
.
pathName
,
title
:
args
.
title
})
}
else
{
// Update the folder title only
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
folder
.
id
).
update
({
title
:
args
.
title
})
}
WIKI
.
logger
.
debug
(
`Renamed folder
${
folder
.
id
}
successfully.`
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Folder renamed successfully'
)
}
}
catch
(
err
)
{
WIKI
.
logger
.
debug
(
`Failed to rename folder
${
args
.
folderId
}
:
${
err
.
message
}
`
)
return
graphHelper
.
generateError
(
err
)
}
},
...
...
@@ -153,7 +236,7 @@ module.exports = {
WIKI
.
logger
.
debug
(
`Deleting folder
${
folder
.
id
}
at path
${
folderPath
}
...`
)
// Delete all children
const
deletedNodes
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'folderPath'
,
'
~'
,
`
${
folderPath
}
.*`
).
del
().
returning
([
'id'
,
'type'
])
const
deletedNodes
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'folderPath'
,
'
<@'
,
folderPath
).
del
().
returning
([
'id'
,
'type'
])
// Delete folders
const
deletedFolders
=
deletedNodes
.
filter
(
n
=>
n
.
type
===
'folder'
).
map
(
n
=>
n
.
id
)
...
...
@@ -179,12 +262,13 @@ module.exports = {
// Delete the folder itself
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
folder
.
id
).
del
()
WIKI
.
logger
.
debug
(
`Delet
ing
folder
${
folder
.
id
}
successfully.`
)
WIKI
.
logger
.
debug
(
`Delet
ed
folder
${
folder
.
id
}
successfully.`
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Folder deleted successfully'
)
}
}
catch
(
err
)
{
WIKI
.
logger
.
debug
(
`Failed to delete folder
${
args
.
folderId
}
:
${
err
.
message
}
`
)
return
graphHelper
.
generateError
(
err
)
}
}
...
...
server/graph/schemas/tree.graphql
View file @
c377eca6
...
...
@@ -15,6 +15,9 @@ extend type Query {
depth
:
Int
includeAncestors
:
Boolean
):
[
TreeItem
]
folderById
(
id
:
UUID
!
):
TreeItemFolder
}
extend
type
Mutation
{
...
...
@@ -39,8 +42,8 @@ extend type Mutation {
):
DefaultResponse
renameFolder
(
folderId
:
UUID
!
pathName
:
String
title
:
String
pathName
:
String
!
title
:
String
!
):
DefaultResponse
}
...
...
ux/public/_assets/icons/fluent-rename.svg
0 → 100644
View file @
c377eca6
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 48 48"
width=
"96px"
height=
"96px"
><linearGradient
id=
"PuVtuXTbHVUsxZgps56lha"
x1=
"4"
x2=
"44"
y1=
"24"
y2=
"24"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#50e6ff"
/><stop
offset=
".55"
stop-color=
"#50e6ff"
/><stop
offset=
".58"
stop-color=
"#4fe3fc"
/><stop
offset=
".601"
stop-color=
"#4edaf4"
/><stop
offset=
".62"
stop-color=
"#4acae7"
/><stop
offset=
".637"
stop-color=
"#46b4d3"
/><stop
offset=
".64"
stop-color=
"#45b0d0"
/><stop
offset=
".71"
stop-color=
"#45b0d0"
/><stop
offset=
".713"
stop-color=
"#46b4d3"
/><stop
offset=
".73"
stop-color=
"#4acae7"
/><stop
offset=
".749"
stop-color=
"#4edaf4"
/><stop
offset=
".77"
stop-color=
"#4fe3fc"
/><stop
offset=
".8"
stop-color=
"#50e6ff"
/><stop
offset=
"1"
stop-color=
"#50e6ff"
/></linearGradient><path
fill=
"url(#PuVtuXTbHVUsxZgps56lha)"
d=
"M4,16v16c0,1.105,0.895,2,2,2h36c1.105,0,2-0.895,2-2V16c0-1.105-0.895-2-2-2H6 C4.895,14,4,14.895,4,16z"
/><path
fill=
"#057093"
d=
"M38,44h-1c-4.418,0-8-3.582-8-8V12c0-4.418,3.582-8,8-8h1c0.552,0,1,0.448,1,1v2 c0,0.552-0.448,1-1,1h-1c-2.209,0-4,1.791-4,4v24c0,2.209,1.791,4,4,4h1c0.552,0,1,0.448,1,1v2C39,43.552,38.552,44,38,44z"
/><path
fill=
"#057093"
d=
"M24,44h1c4.418,0,8-3.582,8-8V12c0-4.418-3.582-8-8-8h-1c-0.552,0-1,0.448-1,1v2 c0,0.552,0.448,1,1,1h1c2.209,0,4,1.791,4,4v24c0,2.209-1.791,4-4,4h-1c-0.552,0-1,0.448-1,1v2C23,43.552,23.448,44,24,44z"
/></svg>
\ No newline at end of file
ux/src/components/FileManager.vue
View file @
c377eca6
This diff is collapsed.
Click to expand it.
ux/src/components/FolderRenameDialog.vue
0 → 100644
View file @
c377eca6
<
template
lang=
"pug"
>
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-rename.svg', left, size='sm')
span
{{
t
(
`fileman.folderRename`
)
}}
q-form.q-py-sm(ref='renameFolderForm', @submit='rename')
q-item
blueprint-icon(icon='folder')
q-item-section
q-input(
outlined
v-model='state.title'
dense
:rules='titleValidation'
hide-bottom-space
:label='t(`fileman.folderTitle`)'
:aria-label='t(`fileman.folderTitle`)'
lazy-rules='ondemand'
autofocus
ref='iptTitle'
@keyup.enter='rename'
)
q-item
blueprint-icon.self-start(icon='file-submodule')
q-item-section
q-input(
outlined
v-model='state.path'
dense
:rules='pathValidation'
hide-bottom-space
:label='t(`fileman.folderFileName`)'
:aria-label='t(`fileman.folderFileName`)'
:hint='t(`fileman.folderFileNameHint`)'
lazy-rules='ondemand'
@focus='state.pathDirty = true'
@keyup.enter='rename'
)
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`common.actions.rename`)'
color='primary'
padding='xs md'
@click='rename'
:loading='state.loading > 0'
)
q-inner-loading(:showing='state.loading > 0')
q-spinner(color='accent', size='lg')
</
template
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
onMounted
,
reactive
,
ref
,
watch
}
from
'vue'
import
slugify
from
'slugify'
import
{
useSiteStore
}
from
'src/stores/site'
// PROPS
const
props
=
defineProps
({
folderId
:
{
type
:
String
,
required
:
true
}
})
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// STORES
const
siteStore
=
useSiteStore
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
path
:
''
,
title
:
''
,
pathDirty
:
false
,
loading
:
false
})
// REFS
const
renameFolderForm
=
ref
(
null
)
const
iptTitle
=
ref
(
null
)
// VALIDATION RULES
const
titleValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'fileman.folderTitleMissing'
),
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'fileman.folderTitleInvalidChars'
)
]
const
pathValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'fileman.folderFileNameMissing'
),
val
=>
/^
[
a-z0-9-
]
+$/
.
test
(
val
)
||
t
(
'fileman.folderFileNameInvalid'
)
]
// 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
rename
()
{
state
.
loading
++
try
{
const
isFormValid
=
await
renameFolderForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
t
(
'fileman.renameFolderInvalidData'
))
}
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation renameFolder (
$folderId: UUID!
$pathName: String!
$title: String!
) {
renameFolder (
folderId: $folderId
pathName: $pathName
title: $title
) {
operation {
succeeded
message
}
}
}
`
,
variables
:
{
folderId
:
props
.
folderId
,
pathName
:
state
.
path
,
title
:
state
.
title
}
})
if
(
resp
?.
data
?.
renameFolder
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'fileman.renameFolderSuccess'
)
})
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
renameFolder
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
state
.
loading
--
}
// MOUNTED
onMounted
(
async
()
=>
{
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query fetchFolderForRename (
$id: UUID!
) {
folderById (
id: $id
) {
id
folderPath
fileName
title
}
}
`
,
variables
:
{
id
:
props
.
folderId
}
})
if
(
resp
?.
data
?.
folderById
?.
id
!==
props
.
folderId
)
{
throw
new
Error
(
'Failed to fetch folder data.'
)
}
state
.
path
=
resp
.
data
.
folderById
.
fileName
state
.
title
=
resp
.
data
.
folderById
.
title
state
.
pathDirty
=
true
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
onDialogCancel
()
}
state
.
loading
--
})
</
script
>
ux/src/components/PageSaveDialog.vue
View file @
c377eca6
...
...
@@ -282,7 +282,7 @@ async function loadTree (parentId, types) {
title
createdAt
updatedAt
pageE
ditor
e
ditor
}
}
}
...
...
ux/src/i18n/locales/en.json
View file @
c377eca6
...
...
@@ -1611,5 +1611,8 @@
"admin.flags.advanced.label"
:
"Custom Configuration"
,
"admin.flags.advanced.hint"
:
"Set custom configuration flags. Note that all values are public to all users! Do not insert senstive data."
,
"admin.flags.saveSuccess"
:
"Flags have been updated successfully."
,
"fileman.copyURLSuccess"
:
"URL has been copied to the clipboard."
"fileman.copyURLSuccess"
:
"URL has been copied to the clipboard."
,
"fileman.folderRename"
:
"Rename Folder"
,
"fileman.renameFolderInvalidData"
:
"One or more fields are invalid."
,
"fileman.renameFolderSuccess"
:
"Folder renamed successfully."
}
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