feat: rename folder + fileman improvements

parent bfbb64a7
......@@ -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)
}
}
......
......@@ -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
}
......
<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
<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>
......@@ -282,7 +282,7 @@ async function loadTree (parentId, types) {
title
createdAt
updatedAt
pageEditor
editor
}
}
}
......
......@@ -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."
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment