diff --git a/server/graph/resolvers/tree.js b/server/graph/resolvers/tree.js
index 37fe081b..eb2bbba8 100644
--- a/server/graph/resolvers/tree.js
+++ b/server/graph/resolvers/tree.js
@@ -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(`Deleting folder ${folder.id} successfully.`)
+ WIKI.logger.debug(`Deleted 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)
}
}
diff --git a/server/graph/schemas/tree.graphql b/server/graph/schemas/tree.graphql
index 25f408fa..79e003a0 100644
--- a/server/graph/schemas/tree.graphql
+++ b/server/graph/schemas/tree.graphql
@@ -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
}
diff --git a/ux/public/_assets/icons/fluent-rename.svg b/ux/public/_assets/icons/fluent-rename.svg
new file mode 100644
index 00000000..e8edc77d
--- /dev/null
+++ b/ux/public/_assets/icons/fluent-rename.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/src/components/FileManager.vue b/ux/src/components/FileManager.vue
index 464f8b8b..c9602352 100644
--- a/ux/src/components/FileManager.vue
+++ b/ux/src/components/FileManager.vue
@@ -37,34 +37,44 @@ q-layout.fileman(view='hHh lpR lFr', container)
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.close`)}}
q-drawer.fileman-left(:model-value='true', :width='350')
- .q-px-md.q-pb-sm
- tree(
- ref='treeComp'
- :nodes='state.treeNodes'
- :roots='state.treeRoots'
- v-model:selected='state.currentFolderId'
- @lazy-load='treeLazyLoad'
- :use-lazy-load='true'
- @context-action='treeContextAction'
- :display-mode='state.displayMode'
+ q-scroll-area(
+ :thumb-style='thumbStyle'
+ :bar-style='barStyle'
+ style='height: 100%;'
)
- q-drawer.fileman-right(:model-value='$q.screen.gt.md', :width='350', side='right')
- .q-pa-md
- template(v-if='currentFileDetails')
- q-img.rounded-borders.q-mb-md(
- src='/_assets/illustrations/fileman-page.svg'
- width='100%'
- fit='cover'
- :ratio='16/10'
- no-spinner
+ .q-px-md.q-pb-sm
+ tree(
+ ref='treeComp'
+ :nodes='state.treeNodes'
+ :roots='state.treeRoots'
+ v-model:selected='state.currentFolderId'
+ @lazy-load='treeLazyLoad'
+ :use-lazy-load='true'
+ @context-action='treeContextAction'
+ :display-mode='state.displayMode'
)
- .fileman-details-row(
- v-for='item of currentFileDetails.items'
+ q-drawer.fileman-right(:model-value='$q.screen.gt.md', :width='350', side='right')
+ q-scroll-area(
+ :thumb-style='thumbStyle'
+ :bar-style='barStyle'
+ style='height: 100%;'
+ )
+ .q-pa-md
+ template(v-if='currentFileDetails')
+ q-img.rounded-borders.q-mb-md(
+ src='/_assets/illustrations/fileman-page.svg'
+ width='100%'
+ fit='cover'
+ :ratio='16/10'
+ no-spinner
)
- label {{item.label}}
- span {{item.value}}
+ .fileman-details-row(
+ v-for='item of currentFileDetails.items'
+ )
+ label {{item.label}}
+ span {{item.value}}
q-page-container
- q-page.fileman-center
+ q-page.fileman-center.column
//- TOOLBAR -----------------------------------------------------
q-toolbar.fileman-toolbar
template(v-if='state.isUploading')
@@ -182,68 +192,79 @@ q-layout.fileman(view='hHh lpR lFr', container)
icon='las la-cloud-upload-alt'
@click='uploadFile'
)
- .fileman-emptylist(v-if='files.length < 1')
- template(v-if='state.fileListLoading')
- q-spinner.q-mr-sm(color='primary', size='xs', :thickness='3')
- span.text-primary Loading...
- template(v-else)
- q-icon.q-mr-sm(name='las la-exclamation-triangle', size='sm')
- span This folder is empty.
- q-list.fileman-filelist(v-else)
- q-item(
- v-for='item of files'
- :key='item.id'
- clickable
- active-class='active'
- :active='item.id === state.currentFileId'
- @click.native='selectItem(item)'
- @dblclick.native='openItem(item)'
- )
- q-item-section.fileman-filelist-icon(avatar)
- q-icon(:name='item.icon', :size='state.isCompact ? `md` : `xl`')
- q-item-section.fileman-filelist-label
- q-item-label {{item.title}}
- q-item-label(caption, v-if='!state.isCompact') {{item.caption}}
- q-item-section.fileman-filelist-side(side, v-if='item.side')
- .text-caption {{item.side}}
- //- RIGHT-CLICK MENU
- q-menu(
- touch-position
- context-menu
- auto-close
- transition-show='jump-down'
- transition-hide='jump-up'
+
+ .row(style='flex: 1 1 100%;')
+ .col
+ q-scroll-area(
+ :thumb-style='thumbStyle'
+ :bar-style='barStyle'
+ style='height: 100%;'
)
- q-card.q-pa-sm
- q-list(dense, style='min-width: 150px;')
- q-item(clickable, v-if='item.type === `page`')
- q-item-section(side)
- q-icon(name='las la-edit', color='orange')
- q-item-section Edit
- q-item(clickable, v-if='item.type !== `folder`', @click='openItem(item)')
- q-item-section(side)
- q-icon(name='las la-eye', color='primary')
- q-item-section View
- q-item(clickable, v-if='item.type !== `folder`', @click='copyItemURL(item)')
- q-item-section(side)
- q-icon(name='las la-clipboard', color='primary')
- q-item-section Copy URL
- q-item(clickable)
- q-item-section(side)
- q-icon(name='las la-copy', color='teal')
- q-item-section Duplicate...
- q-item(clickable)
- q-item-section(side)
- q-icon(name='las la-redo', color='teal')
- q-item-section Rename...
- q-item(clickable)
- q-item-section(side)
- q-icon(name='las la-arrow-right', color='teal')
- q-item-section Move to...
- q-item(clickable, @click='delItem(item)')
- q-item-section(side)
- q-icon(name='las la-trash-alt', color='negative')
- q-item-section.text-negative Delete
+ .fileman-emptylist(v-if='files.length < 1')
+ template(v-if='state.fileListLoading')
+ q-spinner.q-mr-sm(color='primary', size='xs', :thickness='3')
+ span.text-primary Loading...
+ template(v-else)
+ q-icon.q-mr-sm(name='las la-folder-open', size='sm')
+ span This folder is empty.
+ q-list.fileman-filelist(
+ v-else
+ :class='state.isCompact && `is-compact`'
+ )
+ q-item(
+ v-for='item of files'
+ :key='item.id'
+ clickable
+ active-class='active'
+ :active='item.id === state.currentFileId'
+ @click.native='selectItem(item)'
+ @dblclick.native='openItem(item)'
+ )
+ q-item-section.fileman-filelist-icon(avatar)
+ q-icon(:name='item.icon', :size='state.isCompact ? `md` : `xl`')
+ q-item-section.fileman-filelist-label
+ q-item-label {{usePathTitle ? item.fileName : item.title}}
+ q-item-label(caption, v-if='!state.isCompact') {{item.caption}}
+ q-item-section.fileman-filelist-side(side, v-if='item.side')
+ .text-caption {{item.side}}
+ //- RIGHT-CLICK MENU
+ q-menu(
+ touch-position
+ context-menu
+ auto-close
+ transition-show='jump-down'
+ transition-hide='jump-up'
+ )
+ q-card.q-pa-sm
+ q-list(dense, style='min-width: 150px;')
+ q-item(clickable, v-if='item.type === `page`')
+ q-item-section(side)
+ q-icon(name='las la-edit', color='orange')
+ q-item-section Edit
+ q-item(clickable, v-if='item.type !== `folder`', @click='openItem(item)')
+ q-item-section(side)
+ q-icon(name='las la-eye', color='primary')
+ q-item-section View
+ q-item(clickable, v-if='item.type !== `folder`', @click='copyItemURL(item)')
+ q-item-section(side)
+ q-icon(name='las la-clipboard', color='primary')
+ q-item-section Copy URL
+ q-item(clickable)
+ q-item-section(side)
+ q-icon(name='las la-copy', color='teal')
+ q-item-section Duplicate...
+ q-item(clickable, @click='renameItem(item)')
+ q-item-section(side)
+ q-icon(name='las la-redo', color='teal')
+ q-item-section Rename...
+ q-item(clickable)
+ q-item-section(side)
+ q-icon(name='las la-arrow-right', color='teal')
+ q-item-section Move to...
+ q-item(clickable, @click='delItem(item)')
+ q-item-section(side)
+ q-icon(name='las la-trash-alt', color='negative')
+ q-item-section.text-negative Delete
q-footer
q-bar.fileman-path
small.text-caption.text-grey-7 {{folderPath}}
@@ -259,7 +280,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
diff --git a/ux/src/components/PageSaveDialog.vue b/ux/src/components/PageSaveDialog.vue
index 357cd8c5..9e921b91 100644
--- a/ux/src/components/PageSaveDialog.vue
+++ b/ux/src/components/PageSaveDialog.vue
@@ -282,7 +282,7 @@ async function loadTree (parentId, types) {
title
createdAt
updatedAt
- pageEditor
+ editor
}
}
}
diff --git a/ux/src/i18n/locales/en.json b/ux/src/i18n/locales/en.json
index 94972dcc..a6b29b20 100644
--- a/ux/src/i18n/locales/en.json
+++ b/ux/src/i18n/locales/en.json
@@ -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."
}