mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 13:17:10 -05:00
feat: file manager improvements + tree db model
This commit is contained in:
+53
-132
@@ -12,6 +12,9 @@ const reTitle = /^[^<>"]+$/
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
/**
|
||||
* FETCH TREE
|
||||
*/
|
||||
async tree (obj, args, context, info) {
|
||||
// Offset
|
||||
const offset = args.offset || 0
|
||||
@@ -20,8 +23,8 @@ module.exports = {
|
||||
}
|
||||
|
||||
// Limit
|
||||
const limit = args.limit || 100
|
||||
if (limit < 1 || limit > 100) {
|
||||
const limit = args.limit || 1000
|
||||
if (limit < 1 || limit > 1000) {
|
||||
throw new Error('Invalid Limit')
|
||||
}
|
||||
|
||||
@@ -53,17 +56,27 @@ module.exports = {
|
||||
.select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
|
||||
.where(builder => {
|
||||
builder.where('folderPath', '~', folderPathCondition)
|
||||
// -> Include ancestors
|
||||
if (args.includeAncestors) {
|
||||
const parentPathParts = parentPath.split('.')
|
||||
for (let i = 1; i <= parentPathParts.length; i++) {
|
||||
builder.orWhere({
|
||||
folderPath: _.dropRight(parentPathParts, i).join('.'),
|
||||
fileName: _.nth(parentPathParts, i * -1)
|
||||
fileName: _.nth(parentPathParts, i * -1),
|
||||
type: 'folder'
|
||||
})
|
||||
}
|
||||
}
|
||||
// -> Include root items
|
||||
if (args.includeRootItems) {
|
||||
builder.orWhere({
|
||||
folderPath: '',
|
||||
type: 'folder'
|
||||
})
|
||||
}
|
||||
})
|
||||
.andWhere(builder => {
|
||||
// -> Limit to specific types
|
||||
if (args.types && args.types.length > 0) {
|
||||
builder.whereIn('type', args.types)
|
||||
}
|
||||
@@ -85,20 +98,52 @@ module.exports = {
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
...(item.type === 'folder') && {
|
||||
childrenCount: 0
|
||||
childrenCount: item.meta?.children || 0
|
||||
}
|
||||
}))
|
||||
},
|
||||
/**
|
||||
* FETCH SINGLE FOLDER BY ID
|
||||
*/
|
||||
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()
|
||||
|
||||
if (!folder) {
|
||||
throw new Error('ERR_FOLDER_NOT_EXIST')
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
|
||||
childrenCount: 0
|
||||
childrenCount: folder.meta?.children || 0
|
||||
}
|
||||
},
|
||||
/**
|
||||
* FETCH SINGLE FOLDER BY PATH
|
||||
*/
|
||||
async folderByPath (obj, args, context) {
|
||||
const parentPathParts = args.path.replaceAll('/', '.').replaceAll('-', '_').split('.')
|
||||
const folder = await WIKI.db.knex('tree')
|
||||
.select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
|
||||
.where({
|
||||
siteId: args.siteId,
|
||||
localeCode: args.locale,
|
||||
folderPath: _.dropRight(parentPathParts).join('.'),
|
||||
fileName: _.last(parentPathParts)
|
||||
})
|
||||
.first()
|
||||
|
||||
if (!folder) {
|
||||
throw new Error('ERR_FOLDER_NOT_EXIST')
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
|
||||
childrenCount: folder.meta?.children || 0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -108,48 +153,8 @@ module.exports = {
|
||||
*/
|
||||
async createFolder (obj, args, context) {
|
||||
try {
|
||||
WIKI.logger.debug(`Creating new folder ${args.pathName}...`)
|
||||
await WIKI.db.tree.createFolder(args)
|
||||
|
||||
// Get parent path
|
||||
let parentPath = ''
|
||||
if (args.parentId) {
|
||||
const parent = await WIKI.db.knex('tree').where('id', args.parentId).first()
|
||||
parentPath = parent ? `${parent.folderPath}.${parent.fileName}` : ''
|
||||
if (parent) {
|
||||
parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
|
||||
}
|
||||
parentPath = parentPath.replaceAll('-', '_')
|
||||
}
|
||||
|
||||
// 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')
|
||||
}
|
||||
|
||||
// Check for collision
|
||||
const existingFolder = await WIKI.db.knex('tree').where({
|
||||
siteId: args.siteId,
|
||||
folderPath: parentPath,
|
||||
fileName: args.pathName
|
||||
}).first()
|
||||
if (existingFolder) {
|
||||
throw new Error('ERR_FOLDER_ALREADY_EXISTS')
|
||||
}
|
||||
|
||||
// Create folder
|
||||
WIKI.logger.debug(`Creating new folder ${args.pathName} at path /${parentPath}...`)
|
||||
await WIKI.db.knex('tree').insert({
|
||||
folderPath: parentPath,
|
||||
fileName: args.pathName,
|
||||
type: 'folder',
|
||||
title: args.title,
|
||||
siteId: args.siteId
|
||||
})
|
||||
return {
|
||||
operation: graphHelper.generateSuccess('Folder created successfully')
|
||||
}
|
||||
@@ -163,59 +168,7 @@ module.exports = {
|
||||
*/
|
||||
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.`)
|
||||
await WIKI.db.tree.renameFolder(args)
|
||||
|
||||
return {
|
||||
operation: graphHelper.generateSuccess('Folder renamed successfully')
|
||||
@@ -230,39 +183,7 @@ module.exports = {
|
||||
*/
|
||||
async deleteFolder (obj, args, context) {
|
||||
try {
|
||||
// Get folder
|
||||
const folder = await WIKI.db.knex('tree').where('id', args.folderId).first()
|
||||
const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
|
||||
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'])
|
||||
|
||||
// Delete folders
|
||||
const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
|
||||
if (deletedFolders.length > 0) {
|
||||
WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
|
||||
}
|
||||
|
||||
// Delete pages
|
||||
const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
|
||||
if (deletedPages.length > 0) {
|
||||
WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
|
||||
|
||||
// TODO: Delete page
|
||||
}
|
||||
|
||||
// Delete assets
|
||||
const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
|
||||
if (deletedAssets.length > 0) {
|
||||
WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
|
||||
|
||||
// TODO: Delete asset
|
||||
}
|
||||
|
||||
// Delete the folder itself
|
||||
await WIKI.db.knex('tree').where('id', folder.id).del()
|
||||
WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
|
||||
await WIKI.db.tree.deleteFolder(args.folderId)
|
||||
|
||||
return {
|
||||
operation: graphHelper.generateSuccess('Folder deleted successfully')
|
||||
|
||||
@@ -14,16 +14,24 @@ extend type Query {
|
||||
orderByDirection: OrderByDirection
|
||||
depth: Int
|
||||
includeAncestors: Boolean
|
||||
includeRootItems: Boolean
|
||||
): [TreeItem]
|
||||
folderById(
|
||||
id: UUID!
|
||||
): TreeItemFolder
|
||||
folderByPath(
|
||||
siteId: UUID!
|
||||
locale: String!
|
||||
path: String!
|
||||
): TreeItemFolder
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createFolder(
|
||||
siteId: UUID!
|
||||
locale: String!
|
||||
parentId: UUID
|
||||
parentPath: String
|
||||
pathName: String!
|
||||
title: String!
|
||||
): DefaultResponse
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
const Model = require('objection').Model
|
||||
const _ = require('lodash')
|
||||
|
||||
const rePathName = /^[a-z0-9-]+$/
|
||||
const reTitle = /^[^<>"]+$/
|
||||
|
||||
/**
|
||||
* Tree model
|
||||
*/
|
||||
module.exports = class Tree extends Model {
|
||||
static get tableName() { return 'tree' }
|
||||
|
||||
static get jsonSchema () {
|
||||
return {
|
||||
type: 'object',
|
||||
required: ['fileName'],
|
||||
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
folderPath: {type: 'string'},
|
||||
fileName: {type: 'string'},
|
||||
type: {type: 'string'},
|
||||
title: {type: 'string'},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get jsonAttributes() {
|
||||
return ['meta']
|
||||
}
|
||||
|
||||
static get relationMappings() {
|
||||
return {
|
||||
locale: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: require('./locales'),
|
||||
join: {
|
||||
from: 'tree.localeCode',
|
||||
to: 'locales.code'
|
||||
}
|
||||
},
|
||||
site: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: require('./sites'),
|
||||
join: {
|
||||
from: 'tree.siteId',
|
||||
to: 'sites.id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$beforeUpdate() {
|
||||
this.updatedAt = new Date().toISOString()
|
||||
}
|
||||
$beforeInsert() {
|
||||
this.createdAt = new Date().toISOString()
|
||||
this.updatedAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create New Folder
|
||||
*
|
||||
* @param {Object} args - New Folder Properties
|
||||
* @param {string} [args.parentId] - UUID of the parent folder
|
||||
* @param {string} [args.parentPath] - Path of the parent folder
|
||||
* @param {string} args.pathName - Path name of the folder to create
|
||||
* @param {string} args.title - Title of the folder to create
|
||||
* @param {string} args.locale - Locale code of the folder to create
|
||||
* @param {string} args.siteId - UUID of the site in which the folder will be created
|
||||
*/
|
||||
static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
|
||||
// Validate path name
|
||||
if (!rePathName.test(pathName)) {
|
||||
throw new Error('ERR_INVALID_PATH_NAME')
|
||||
}
|
||||
|
||||
// Validate title
|
||||
if (!reTitle.test(title)) {
|
||||
throw new Error('ERR_INVALID_TITLE')
|
||||
}
|
||||
|
||||
WIKI.logger.debug(`Creating new folder ${pathName}...`)
|
||||
parentPath = parentPath?.replaceAll('/', '.')?.replaceAll('-', '_') || ''
|
||||
const parentPathParts = parentPath.split('.')
|
||||
const parentFilter = {
|
||||
folderPath: _.dropRight(parentPathParts).join('.'),
|
||||
fileName: _.last(parentPathParts)
|
||||
}
|
||||
|
||||
// Get parent path
|
||||
let parent = null
|
||||
if (parentId) {
|
||||
parent = await WIKI.db.knex('tree').where('id', parentId).first()
|
||||
if (!parent) {
|
||||
throw new Error('ERR_NONEXISTING_PARENT_ID')
|
||||
}
|
||||
parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
|
||||
} else if (parentPath) {
|
||||
parent = await WIKI.db.knex('tree').where(parentFilter).first()
|
||||
} else {
|
||||
parentPath = ''
|
||||
}
|
||||
|
||||
// Check for collision
|
||||
const existingFolder = await WIKI.db.knex('tree').where({
|
||||
siteId: siteId,
|
||||
localeCode: locale,
|
||||
folderPath: parentPath,
|
||||
fileName: pathName
|
||||
}).first()
|
||||
if (existingFolder) {
|
||||
throw new Error('ERR_FOLDER_ALREADY_EXISTS')
|
||||
}
|
||||
|
||||
// Ensure all ancestors exist
|
||||
if (parentPath) {
|
||||
const expectedAncestors = []
|
||||
const existingAncestors = await WIKI.db.knex('tree').select('folderPath', 'fileName').where(builder => {
|
||||
const parentPathParts = parentPath.split('.')
|
||||
for (let i = 1; i <= parentPathParts.length; i++) {
|
||||
const ancestor = {
|
||||
folderPath: _.dropRight(parentPathParts, i).join('.'),
|
||||
fileName: _.nth(parentPathParts, i * -1)
|
||||
}
|
||||
expectedAncestors.push(ancestor)
|
||||
builder.orWhere({
|
||||
...ancestor,
|
||||
type: 'folder'
|
||||
})
|
||||
}
|
||||
})
|
||||
for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
|
||||
WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
|
||||
const newAncestor = await WIKI.db.knex('tree').insert({
|
||||
...ancestor,
|
||||
type: 'folder',
|
||||
title: ancestor.fileName,
|
||||
localeCode: locale,
|
||||
siteId: siteId,
|
||||
meta: {
|
||||
children: 1
|
||||
}
|
||||
}).returning('*')
|
||||
|
||||
// Parent didn't exist until now, assign it
|
||||
if (!parent && ancestor.folderPath === parentFilter.folderPath && ancestor.fileName === parentFilter.fileName) {
|
||||
parent = newAncestor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create folder
|
||||
WIKI.logger.debug(`Creating new folder ${pathName} at path /${parentPath}...`)
|
||||
await WIKI.db.knex('tree').insert({
|
||||
folderPath: parentPath,
|
||||
fileName: pathName,
|
||||
type: 'folder',
|
||||
title: title,
|
||||
localeCode: locale,
|
||||
siteId: siteId,
|
||||
meta: {
|
||||
children: 0
|
||||
}
|
||||
})
|
||||
|
||||
// Update parent ancestor count
|
||||
if (parent) {
|
||||
await WIKI.db.knex('tree').where('id', parent.id).update({
|
||||
meta: {
|
||||
...(parent.meta ?? {}),
|
||||
children: (parent.meta?.children || 0) + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a folder
|
||||
*
|
||||
* @param {Object} args - Rename Folder Properties
|
||||
* @param {string} args.folderId - UUID of the folder to rename
|
||||
* @param {string} args.pathName - New path name of the folder
|
||||
* @param {string} args.title - New title of the folder
|
||||
*/
|
||||
static async renameFolder ({ folderId, pathName, title }) {
|
||||
// Get folder
|
||||
const folder = await WIKI.db.knex('tree').where('id', folderId).first()
|
||||
if (!folder) {
|
||||
throw new Error('ERR_NONEXISTING_FOLDER_ID')
|
||||
}
|
||||
|
||||
// Validate path name
|
||||
if (!rePathName.test(pathName)) {
|
||||
throw new Error('ERR_INVALID_PATH_NAME')
|
||||
}
|
||||
|
||||
// Validate title
|
||||
if (!reTitle.test(title)) {
|
||||
throw new Error('ERR_INVALID_TITLE')
|
||||
}
|
||||
|
||||
WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
|
||||
|
||||
if (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: 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}.${pathName}` : 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: pathName,
|
||||
title: title
|
||||
})
|
||||
} else {
|
||||
// Update the folder title only
|
||||
await WIKI.db.knex('tree').where('id', folder.id).update({
|
||||
title: title
|
||||
})
|
||||
}
|
||||
|
||||
WIKI.logger.debug(`Renamed folder ${folder.id} successfully.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a folder
|
||||
*
|
||||
* @param {String} folderId Folder ID
|
||||
*/
|
||||
static async deleteFolder (folderId) {
|
||||
// Get folder
|
||||
const folder = await WIKI.db.knex('tree').where('id', folderId).first()
|
||||
if (!folder) {
|
||||
throw new Error('ERR_NONEXISTING_FOLDER_ID')
|
||||
}
|
||||
const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
|
||||
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'])
|
||||
|
||||
// Delete folders
|
||||
const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
|
||||
if (deletedFolders.length > 0) {
|
||||
WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
|
||||
}
|
||||
|
||||
// Delete pages
|
||||
const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
|
||||
if (deletedPages.length > 0) {
|
||||
WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
|
||||
|
||||
// TODO: Delete page
|
||||
}
|
||||
|
||||
// Delete assets
|
||||
const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
|
||||
if (deletedAssets.length > 0) {
|
||||
WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
|
||||
|
||||
// TODO: Delete asset
|
||||
}
|
||||
|
||||
// Delete the folder itself
|
||||
await WIKI.db.knex('tree').where('id', folder.id).del()
|
||||
|
||||
// Update parent children count
|
||||
if (folder.folderPath) {
|
||||
const parentPathParts = folder.folderPath.split('.')
|
||||
const parent = await WIKI.db.knex('tree').where({
|
||||
folderPath: _.dropRight(parentPathParts).join('.'),
|
||||
fileName: _.last(parentPathParts)
|
||||
}).first()
|
||||
await WIKI.db.knex('tree').where('id', parent.id).update({
|
||||
meta: {
|
||||
...(parent.meta ?? {}),
|
||||
children: (parent.meta?.children || 1) - 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100px" height="100px"><path d="M 18 27 A 1.0001 1.0001 0 0 0 17 28 L 17 75 C 17 80.511334 21.488666 85 27 85 L 72 85 C 77.511334 85 82 80.511334 82 75 L 82 28 A 1.0001 1.0001 0 0 0 81 27 L 18 27 z M 19 29 L 49 29 L 49 36 L 39.5 36 C 38.125015 36 37 37.125015 37 38.5 L 37 40.5 C 37 41.874985 38.125015 43 39.5 43 L 49 43 L 49 78 L 30.5 78 C 26.904219 78 24 75.095781 24 71.5 L 24 32.5 A 0.50005 0.50005 0 0 0 23.492188 31.992188 A 0.50005 0.50005 0 0 0 23 32.5 L 23 71.5 C 23 75.636219 26.363781 79 30.5 79 L 49.419922 79 A 0.50005 0.50005 0 0 0 49.582031 79 L 68.5 79 C 72.636219 79 76 75.636219 76 71.5 L 76 43.5 A 0.50005 0.50005 0 1 0 75 43.5 L 75 71.5 C 75 75.095781 72.095781 78 68.5 78 L 50 78 L 50 43 L 59.5 43 C 60.874985 43 62 41.874985 62 40.5 L 62 38.5 C 62 37.125015 60.874985 36 59.5 36 L 50 36 L 50 29 L 80 29 L 80 75 C 80 79.430666 76.430666 83 72 83 L 27 83 C 22.569334 83 19 79.430666 19 75 L 19 29 z M 75.492188 31.992188 A 0.50005 0.50005 0 0 0 75 32.5 L 75 38.5 A 0.50005 0.50005 0 1 0 76 38.5 L 76 32.5 A 0.50005 0.50005 0 0 0 75.492188 31.992188 z M 39.5 37 L 59.5 37 C 60.335015 37 61 37.664985 61 38.5 L 61 40.5 C 61 41.335015 60.335015 42 59.5 42 L 49.580078 42 A 0.50005 0.50005 0 0 0 49.417969 42 L 39.5 42 C 38.664985 42 38 41.335015 38 40.5 L 38 38.5 C 38 37.664985 38.664985 37 39.5 37 z M 31.492188 60.992188 A 0.50005 0.50005 0 0 0 31.097656 61.195312 L 29.146484 63.146484 A 0.50005 0.50005 0 1 0 29.853516 63.853516 L 31 62.707031 L 31 68.5 A 0.50005 0.50005 0 1 0 32 68.5 L 32 62.707031 L 33.146484 63.853516 A 0.50005 0.50005 0 1 0 33.853516 63.146484 L 31.898438 61.191406 A 0.50005 0.50005 0 0 0 31.492188 60.992188 z M 38.492188 60.992188 A 0.50005 0.50005 0 0 0 38.097656 61.195312 L 36.146484 63.146484 A 0.50005 0.50005 0 1 0 36.853516 63.853516 L 38 62.707031 L 38 68.5 A 0.50005 0.50005 0 1 0 39 68.5 L 39 62.707031 L 40.146484 63.853516 A 0.50005 0.50005 0 1 0 40.853516 63.146484 L 38.898438 61.191406 A 0.50005 0.50005 0 0 0 38.492188 60.992188 z M 28.5 70 A 0.50005 0.50005 0 1 0 28.5 71 L 41.5 71 A 0.50005 0.50005 0 1 0 41.5 70 L 28.5 70 z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,31 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" width="96px" height="96px">
|
||||
<defs>
|
||||
<linearGradient id="p0leOTPLvuNkjL_fSa~qVa" x1="24" x2="24" y1="9.109" y2="13.568" data-name="Безымянный градиент 6" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#0077d2"/>
|
||||
<stop offset="1" stop-color="#0b59a2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="p0leOTPLvuNkjL_fSa~qVb" x1="4.5" x2="4.5" y1="26.717" y2="41.786" xlink:href="#p0leOTPLvuNkjL_fSa~qVa"/>
|
||||
<linearGradient id="p0leOTPLvuNkjL_fSa~qVc" x1="43.5" x2="43.5" y1="26.717" y2="41.786" xlink:href="#p0leOTPLvuNkjL_fSa~qVa"/>
|
||||
<linearGradient id="p0leOTPLvuNkjL_fSa~qVd" x1="16" x2="16" y1="25.054" y2="43.495" xlink:href="#p0leOTPLvuNkjL_fSa~qVa"/>
|
||||
<linearGradient id="p0leOTPLvuNkjL_fSa~qVe" x1="32" x2="32" y1="25.054" y2="43.495" xlink:href="#p0leOTPLvuNkjL_fSa~qVa"/>
|
||||
</defs>
|
||||
<rect width="2" height="6" x="23" y="8" fill="url(#p0leOTPLvuNkjL_fSa~qVa)"/>
|
||||
<path fill="url(#p0leOTPLvuNkjL_fSa~qVb)" d="M6,27H8a0,0,0,0,1,0,0V37a0,0,0,0,1,0,0H6a5,5,0,0,1-5-5v0A5,5,0,0,1,6,27Z"/>
|
||||
<path fill="url(#p0leOTPLvuNkjL_fSa~qVc)" d="M40,27h2a5,5,0,0,1,5,5v0a5,5,0,0,1-5,5H40a0,0,0,0,1,0,0V27A0,0,0,0,1,40,27Z"/>
|
||||
<path fill="#199be2" d="M24,13h0A18,18,0,0,1,42,31v8a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2V31A18,18,0,0,1,24,13Z"/>
|
||||
<circle cx="16" cy="31" r="6" fill="url(#p0leOTPLvuNkjL_fSa~qVd)"/>
|
||||
<circle cx="32" cy="31" r="6" fill="url(#p0leOTPLvuNkjL_fSa~qVe)"/>
|
||||
<circle cx="32" cy="31" r="4" fill="#50e6ff"/>
|
||||
<circle cx="32" cy="31" r="2" fill="url(#p0leOTPLvuNkjL_fSa~qVe)"/>
|
||||
<circle cx="16" cy="31" r="4" fill="#50e6ff"/>
|
||||
<circle cx="16" cy="31" r="2" fill="url(#p0leOTPLvuNkjL_fSa~qVd)"/>
|
||||
<circle cx="24" cy="8" r="2" fill="#199be2"/>
|
||||
<circle cx="24" cy="8" r="3" fill="#02C39A">
|
||||
<animate attributeName="opacity" dur="1.5s" values="0;1;0;0" repeatCount="indefinite" begin="0.1" />
|
||||
</circle>
|
||||
<circle cx="24" cy="8" r="3" fill="#f99d4d">
|
||||
<animate attributeName="opacity" dur="1.5s" values="0;0;1;0" repeatCount="indefinite" begin="0.1" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -5,6 +5,12 @@ q-layout.fileman(view='hHh lpR lFr', container)
|
||||
q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md')
|
||||
span {{t(`fileman.title`)}}
|
||||
q-toolbar(dark)
|
||||
q-btn.q-mr-sm.acrylic-btn(
|
||||
flat
|
||||
color='white'
|
||||
label='EN'
|
||||
style='height: 40px;'
|
||||
)
|
||||
q-input(
|
||||
dark
|
||||
v-model='state.search'
|
||||
@@ -200,13 +206,12 @@ q-layout.fileman(view='hHh lpR lFr', container)
|
||||
:bar-style='barStyle'
|
||||
style='height: 100%;'
|
||||
)
|
||||
.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.
|
||||
.fileman-loadinglist(v-if='state.fileListLoading')
|
||||
q-spinner.q-mr-sm(color='primary', size='64px', :thickness='1')
|
||||
span.text-primary Fetching folder contents...
|
||||
.fileman-emptylist(v-else-if='files.length < 1')
|
||||
img(src='/_assets/icons/carbon-copy-empty-box.svg')
|
||||
span This folder is empty.
|
||||
q-list.fileman-filelist(
|
||||
v-else
|
||||
:class='state.isCompact && `is-compact`'
|
||||
@@ -394,8 +399,9 @@ const files = computed(() => {
|
||||
}).map(f => {
|
||||
switch (f.type) {
|
||||
case 'folder': {
|
||||
console.info(f.children)
|
||||
f.icon = fileTypes.folder.icon
|
||||
f.caption = t('fileman.folderChildrenCount', f.children, { count: f.children })
|
||||
f.caption = t('fileman.folderChildrenCount', { count: f.children }, f.children)
|
||||
break
|
||||
}
|
||||
case 'page': {
|
||||
@@ -475,7 +481,7 @@ const currentFileDetails = computed(() => {
|
||||
// WATCHERS
|
||||
|
||||
watch(() => state.currentFolderId, async (newValue) => {
|
||||
await loadTree(newValue)
|
||||
await loadTree({ parentId: newValue })
|
||||
})
|
||||
|
||||
// METHODS
|
||||
@@ -485,11 +491,11 @@ function close () {
|
||||
}
|
||||
|
||||
async function treeLazyLoad (nodeId, { done, fail }) {
|
||||
await loadTree(nodeId, ['folder'])
|
||||
await loadTree({ parentId: nodeId, types: ['folder'] })
|
||||
done()
|
||||
}
|
||||
|
||||
async function loadTree (parentId, types) {
|
||||
async function loadTree ({ parentId = null, parentPath = null, types, initLoad = false }) {
|
||||
if (state.isFetching) { return }
|
||||
state.isFetching = true
|
||||
if (!parentId) {
|
||||
@@ -579,7 +585,7 @@ async function loadTree (parentId, types) {
|
||||
type: 'folder',
|
||||
title: item.title,
|
||||
fileName: item.fileName,
|
||||
children: 0
|
||||
children: item.childrenCount || 0
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -664,7 +670,7 @@ function newFolder (parentId) {
|
||||
parentId
|
||||
}
|
||||
}).onOk(() => {
|
||||
loadTree(parentId)
|
||||
loadTree({ parentId })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -676,7 +682,7 @@ function renameFolder (folderId) {
|
||||
}
|
||||
}).onOk(() => {
|
||||
treeComp.value.resetLoaded()
|
||||
loadTree(folderId)
|
||||
loadTree({ parentId: folderId })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -698,13 +704,13 @@ function delFolder (folderId, mustReload = false) {
|
||||
state.treeRoots = state.treeRoots.filter(n => n !== folderId)
|
||||
}
|
||||
if (mustReload) {
|
||||
loadTree(state.currentFolderId, null)
|
||||
loadTree({ parentId: state.currentFolderId })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reloadFolder (folderId) {
|
||||
loadTree(folderId, null)
|
||||
loadTree({ parentId: folderId })
|
||||
treeComp.value.resetLoaded()
|
||||
}
|
||||
|
||||
@@ -899,7 +905,7 @@ function delItem (item) {
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
loadTree()
|
||||
loadTree({})
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -956,17 +962,43 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-emptylist {
|
||||
&-loadinglist {
|
||||
padding: 16px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> span {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-emptylist {
|
||||
padding: 16px;
|
||||
font-style: italic;
|
||||
font-size: 1.5em;
|
||||
font-weight: 300;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> img {
|
||||
opacity: .25;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@at-root .body--light & {
|
||||
color: $grey-6;
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
color: $dark-4;
|
||||
color: $grey-7;
|
||||
|
||||
> img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,12 +143,14 @@ async function create () {
|
||||
mutation: gql`
|
||||
mutation createFolder (
|
||||
$siteId: UUID!
|
||||
$locale: String!
|
||||
$parentId: UUID
|
||||
$pathName: String!
|
||||
$title: String!
|
||||
) {
|
||||
createFolder (
|
||||
siteId: $siteId
|
||||
locale: $locale
|
||||
parentId: $parentId
|
||||
pathName: $pathName
|
||||
title: $title
|
||||
@@ -162,6 +164,7 @@ async function create () {
|
||||
`,
|
||||
variables: {
|
||||
siteId: siteStore.id,
|
||||
locale: 'en',
|
||||
parentId: props.parentId,
|
||||
pathName: state.path,
|
||||
title: state.title
|
||||
|
||||
@@ -81,7 +81,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
|
||||
:color='state.displayMode === `path` ? `positive` : `grey`'
|
||||
size='xs'
|
||||
)
|
||||
q-item-section.q-pr-sm Browse Using Paths
|
||||
q-item-section.q-pr-sm {{ t('pageSaveDialog.displayModePath') }}
|
||||
q-item(clickable, @click='state.displayMode = `title`')
|
||||
q-item-section(side)
|
||||
q-icon(
|
||||
@@ -89,7 +89,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
|
||||
:color='state.displayMode === `title` ? `positive` : `grey`'
|
||||
size='xs'
|
||||
)
|
||||
q-item-section.q-pr-sm Browse Using Titles
|
||||
q-item-section.q-pr-sm {{ t('pageSaveDialog.displayModeTitle') }}
|
||||
q-space
|
||||
q-btn.acrylic-btn(
|
||||
icon='las la-times'
|
||||
@@ -131,18 +131,24 @@ const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'save'
|
||||
default: 'pageSave'
|
||||
},
|
||||
pageId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
pageName: {
|
||||
itemId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
pagePath: {
|
||||
folderPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
itemTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
itemFileName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
@@ -177,33 +183,12 @@ const state = reactive({
|
||||
currentFileId: '',
|
||||
treeNodes: {},
|
||||
treeRoots: [],
|
||||
fileList: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'folder',
|
||||
title: 'Beep Boop'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'folder',
|
||||
title: 'Second Folder'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'page',
|
||||
title: 'Some Page',
|
||||
pageType: 'markdown'
|
||||
}
|
||||
],
|
||||
fileList: [],
|
||||
title: '',
|
||||
path: ''
|
||||
path: '',
|
||||
typesToFetch: []
|
||||
})
|
||||
|
||||
const displayModes = [
|
||||
{ value: 'title', label: t('pageSaveDialog.displayModeTitle') },
|
||||
{ value: 'path', label: t('pageSaveDialog.displayModePath') }
|
||||
]
|
||||
|
||||
const thumbStyle = {
|
||||
right: '1px',
|
||||
borderRadius: '5px',
|
||||
@@ -249,23 +234,32 @@ async function save () {
|
||||
}
|
||||
|
||||
async function treeLazyLoad (nodeId, { done, fail }) {
|
||||
await loadTree(nodeId, ['folder', 'page'])
|
||||
await loadTree({
|
||||
parentId: nodeId,
|
||||
types: ['folder', 'page']
|
||||
})
|
||||
done()
|
||||
}
|
||||
|
||||
async function loadTree (parentId, types) {
|
||||
async function loadTree ({ parentId = null, parentPath = null, types, initLoad = false }) {
|
||||
try {
|
||||
const resp = await APOLLO_CLIENT.query({
|
||||
query: gql`
|
||||
query loadTree (
|
||||
$siteId: UUID!
|
||||
$parentId: UUID
|
||||
$parentPath: String
|
||||
$types: [TreeItemType]
|
||||
$includeAncestors: Boolean
|
||||
$includeRootItems: Boolean
|
||||
) {
|
||||
tree (
|
||||
siteId: $siteId
|
||||
parentId: $parentId
|
||||
parentPath: $parentPath
|
||||
types: $types
|
||||
includeAncestors: $includeAncestors
|
||||
includeRootItems: $includeRootItems
|
||||
) {
|
||||
__typename
|
||||
... on TreeItemFolder {
|
||||
@@ -290,7 +284,10 @@ async function loadTree (parentId, types) {
|
||||
variables: {
|
||||
siteId: siteStore.id,
|
||||
parentId,
|
||||
types
|
||||
parentPath,
|
||||
types,
|
||||
includeAncestors: initLoad,
|
||||
includeRootItems: initLoad
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
@@ -344,16 +341,26 @@ function newFolder (parentId) {
|
||||
parentId
|
||||
}
|
||||
}).onOk(() => {
|
||||
loadTree(parentId)
|
||||
loadTree({ parentId })
|
||||
})
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
loadTree()
|
||||
state.title = props.pageName || ''
|
||||
state.path = props.pagePath || ''
|
||||
switch (props.mode) {
|
||||
case 'pageSave': {
|
||||
state.typesToFetch = ['folder', 'page']
|
||||
break
|
||||
}
|
||||
}
|
||||
loadTree({
|
||||
parentPath: props.folderPath,
|
||||
types: state.typesToFetch,
|
||||
initLoad: true
|
||||
})
|
||||
state.title = props.itemTitle || ''
|
||||
state.path = props.itemFileName || ''
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -1600,8 +1600,8 @@
|
||||
"common.actions.newFolder": "New Folder",
|
||||
"common.actions.duplicate": "Duplicate",
|
||||
"common.actions.moveTo": "Move To",
|
||||
"pageSaveDialog.displayModeTitle": "Title",
|
||||
"pageSaveDialog.displayModePath": "Path",
|
||||
"pageSaveDialog.displayModeTitle": "Browse Using Titles",
|
||||
"pageSaveDialog.displayModePath": "Browse Using Paths",
|
||||
"folderDeleteDialog.title": "Confirm Delete Folder",
|
||||
"folderDeleteDialog.confirm": "Are you sure you want to delete folder {name} and all its content?",
|
||||
"folderDeleteDialog.folderId": "Folder ID {id}",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
q-page.admin-terminal
|
||||
.row.q-pa-md.items-center
|
||||
.col-auto
|
||||
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-bot.svg')
|
||||
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-bot-animated.svg')
|
||||
.col.q-pl-md
|
||||
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.scheduler.title') }}
|
||||
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.scheduler.subtitle') }}
|
||||
|
||||
+12
-10
@@ -466,12 +466,13 @@ function togglePageData () {
|
||||
|
||||
function duplicatePage () {
|
||||
$q.dialog({
|
||||
component: defineAsyncComponent(() => import('../components/PageSaveDialog.vue')),
|
||||
component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
|
||||
componentProps: {
|
||||
mode: 'duplicate',
|
||||
pageId: pageStore.id,
|
||||
pageName: pageStore.title,
|
||||
pagePath: pageStore.path
|
||||
mode: 'duplicatePage',
|
||||
folderPath: '',
|
||||
itemId: pageStore.id,
|
||||
itemTitle: pageStore.title,
|
||||
itemFileName: pageStore.path
|
||||
}
|
||||
}).onOk(() => {
|
||||
// TODO: change route to new location
|
||||
@@ -480,12 +481,13 @@ function duplicatePage () {
|
||||
|
||||
function renamePage () {
|
||||
$q.dialog({
|
||||
component: defineAsyncComponent(() => import('../components/PageSaveDialog.vue')),
|
||||
component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
|
||||
componentProps: {
|
||||
mode: 'rename',
|
||||
pageId: pageStore.id,
|
||||
pageName: pageStore.title,
|
||||
pagePath: pageStore.path
|
||||
mode: 'renamePage',
|
||||
folderPath: '',
|
||||
itemId: pageStore.id,
|
||||
itemTitle: pageStore.title,
|
||||
itemFileName: pageStore.path
|
||||
}
|
||||
}).onOk(() => {
|
||||
// TODO: change route to new location
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import gql from 'graphql-tag'
|
||||
import { cloneDeep, last, pick, transform } from 'lodash-es'
|
||||
import { cloneDeep, initial, last, pick, transform } from 'lodash-es'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import { useSiteStore } from './site'
|
||||
@@ -131,6 +131,9 @@ export const usePageStore = defineStore('page', {
|
||||
path: (last(result)?.path || pathPrefix) + `/${value}`
|
||||
})
|
||||
}, [])
|
||||
},
|
||||
folderPath: (state) => {
|
||||
return initial(state.path.split('/')).join('/')
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
Reference in New Issue
Block a user