feat: rename folder + fileman improvements

pull/6078/head
Nicolas Giard 2 years ago
parent bfbb64a749
commit c377eca6c7
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -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
}

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -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)
<script setup>
import { useI18n } from 'vue-i18n'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { computed, defineAsyncComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { filesize } from 'filesize'
import { useQuasar } from 'quasar'
import { DateTime } from 'luxon'
@ -278,6 +299,7 @@ import { useSiteStore } from 'src/stores/site'
import FolderCreateDialog from 'src/components/FolderCreateDialog.vue'
import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue'
import FolderRenameDialog from 'src/components/FolderRenameDialog.vue'
// QUASAR
@ -301,6 +323,7 @@ const { t } = useI18n()
const state = reactive({
loading: 0,
isFetching: false,
search: '',
currentFolderId: null,
currentFileId: null,
@ -316,6 +339,19 @@ const state = reactive({
fileListLoading: false
})
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: '#000',
width: '5px',
opacity: 0.15
}
const barStyle = {
backgroundColor: '#FAFAFA',
width: '9px',
opacity: 1
}
// REFS
const fileIpt = ref(null)
@ -332,6 +368,8 @@ const folderPath = computed(() => {
}
})
const usePathTitle = computed(() => state.displayMode === 'path')
const filteredFiles = computed(() => {
if (state.search) {
const fuse = new Fuse(state.fileList, {
@ -348,7 +386,6 @@ const filteredFiles = computed(() => {
const files = computed(() => {
return filteredFiles.value.filter(f => {
console.info(f)
// -> Show Folders Filter
if (f.type === 'folder' && !state.shouldShowFolders) {
return false
@ -453,6 +490,8 @@ async function treeLazyLoad (nodeId, { done, fail }) {
}
async function loadTree (parentId, types) {
if (state.isFetching) { return }
state.isFetching = true
if (!parentId) {
parentId = null
}
@ -517,13 +556,11 @@ async function loadTree (parentId, types) {
switch (item.__typename) {
case 'TreeItemFolder': {
// -> Tree Nodes
if (!state.treeNodes[item.id]) {
state.treeNodes[item.id] = {
folderPath: item.folderPath,
fileName: item.fileName,
title: item.title,
children: state.treeNodes[item.id]?.children ?? []
}
state.treeNodes[item.id] = {
folderPath: item.folderPath,
fileName: item.fileName,
title: item.title,
children: state.treeNodes[item.id]?.children ?? []
}
// -> Set Ancestors / Tree Roots
@ -596,6 +633,7 @@ async function loadTree (parentId, types) {
if (parentId) {
treeComp.value.setLoaded(parentId)
}
state.isFetching = false
}
function treeContextAction (nodeId, action) {
@ -604,6 +642,10 @@ function treeContextAction (nodeId, action) {
newFolder(nodeId)
break
}
case 'rename': {
renameFolder(nodeId)
break
}
case 'del': {
delFolder(nodeId)
break
@ -626,6 +668,18 @@ function newFolder (parentId) {
})
}
function renameFolder (folderId) {
$q.dialog({
component: FolderRenameDialog,
componentProps: {
folderId
}
}).onOk(() => {
treeComp.value.resetLoaded()
loadTree(folderId)
})
}
function delFolder (folderId, mustReload = false) {
$q.dialog({
component: FolderDeleteDialog,
@ -654,6 +708,21 @@ function reloadFolder (folderId) {
treeComp.value.resetLoaded()
}
// PAGE METHODS
// --------------------------------------
function delPage (pageId, pageName) {
$q.dialog({
component: defineAsyncComponent(() => import('src/components/PageDeleteDialog.vue')),
componentProps: {
pageId,
pageName
}
}).onOk(() => {
loadTree(state.currentFolderId, null)
})
}
// --------------------------------------
// UPLOAD METHODS
// --------------------------------------
@ -796,12 +865,34 @@ async function copyItemURL (item) {
}
}
function renameItem (item) {
console.info(item)
switch (item.type) {
case 'folder': {
renameFolder(item.id)
break
}
case 'page': {
// TODO: Rename page
break
}
case 'asset': {
// TODO: Rename asset
break
}
}
}
function delItem (item) {
switch (item.type) {
case 'folder': {
delFolder(item.id, true)
break
}
case 'page': {
delPage(item.id, item.title)
break
}
}
}
@ -825,6 +916,7 @@ onMounted(() => {
}
&-center {
@at-root .body--light & {
background-color: #FFF;
}
@ -860,6 +952,10 @@ onMounted(() => {
}
}
&-main {
height: 100%;
}
&-emptylist {
padding: 16px;
font-style: italic;
@ -878,7 +974,7 @@ onMounted(() => {
padding: 8px 12px;
> .q-item {
padding: 8px 6px;
padding: 4px 6px;
border-radius: 8px;
&.active {
@ -894,6 +990,18 @@ onMounted(() => {
}
}
}
&.is-compact {
> .q-item {
padding: 0 6px;
min-height: 36px;
}
.fileman-filelist-icon {
padding-right: 6px;
min-width: 0;
}
}
}
&-details-row {
display: flex;

@ -0,0 +1,226 @@
<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."
}

Loading…
Cancel
Save