From 291fe262721d10f3fac064b1f7d2844beeab4710 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 21 Jan 2024 02:32:42 +0000 Subject: [PATCH] feat: asset rename + asset delete dialogs + linting fixes --- .vscode/settings.json | 2 +- server/graph/resolvers/asset.mjs | 139 ++++++++------- server/graph/schemas/asset.graphql | 6 +- server/locales/en.json | 9 + server/models/assets.mjs | 6 +- server/models/pages.mjs | 34 ++-- server/package.json | 1 + server/pnpm-lock.yaml | 61 +++++++ server/templates/demo/home.md | 31 ++++ ux/.eslintrc.js | 12 +- ux/jsconfig.json | 2 +- ux/package.json | 3 +- ux/pnpm-lock.yaml | 23 +++ ux/public/_assets/icons/ultraviolet-image.svg | 1 + ux/src/components/ApiKeyCopyDialog.vue | 4 +- ux/src/components/ApiKeyCreateDialog.vue | 4 +- ux/src/components/AssetDeleteDialog.vue | 109 ++++++++++++ ux/src/components/AssetRenameDialog.vue | 165 ++++++++++++++++++ ux/src/components/FileManager.vue | 69 ++++++-- ux/src/components/FooterNav.vue | 2 + ux/src/components/HeaderSearch.vue | 1 + ux/src/components/IconPickerDialog.vue | 6 +- ux/src/components/LocaleSelectorMenu.vue | 1 + ux/src/components/NavSidebar.vue | 1 + ux/src/components/PageActionsCol.vue | 5 +- ux/src/components/PageTags.vue | 3 +- ux/src/components/SocialSharingMenu.vue | 2 +- ux/src/components/TreeBrowserDialog.vue | 12 +- ux/src/layouts/AdminLayout.vue | 1 + ux/src/layouts/MainLayout.vue | 1 - ux/src/pages/AdminExtensions.vue | 2 +- ux/src/pages/AdminFlags.vue | 1 - ux/src/pages/AdminLocale.vue | 2 +- ux/src/pages/AdminLogin.vue | 1 - ux/src/pages/AdminMetrics.vue | 4 +- ux/src/pages/AdminSystem.vue | 1 - ux/src/pages/AdminTheme.vue | 2 +- ux/src/pages/AdminUsers.vue | 1 - ux/src/pages/AdminUtilities.vue | 6 - ux/src/pages/Index.vue | 2 +- ux/src/pages/Search.vue | 9 +- ux/src/stores/site.js | 2 +- 42 files changed, 597 insertions(+), 152 deletions(-) create mode 100644 server/templates/demo/home.md create mode 100644 ux/public/_assets/icons/ultraviolet-image.svg create mode 100644 ux/src/components/AssetDeleteDialog.vue create mode 100644 ux/src/components/AssetRenameDialog.vue diff --git a/.vscode/settings.json b/.vscode/settings.json index b606fec0..bec11041 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "vue" ], "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "i18n-ally.localesPaths": [ "server/locales", diff --git a/server/graph/resolvers/asset.mjs b/server/graph/resolvers/asset.mjs index c10412ba..58ac299c 100644 --- a/server/graph/resolvers/asset.mjs +++ b/server/graph/resolvers/asset.mjs @@ -1,6 +1,7 @@ import _ from 'lodash-es' import sanitize from 'sanitize-filename' import { generateError, generateSuccess } from '../../helpers/graph.mjs' +import { decodeFolderPath, decodeTreePath, generateHash } from '../../helpers/common.mjs' import path from 'node:path' import fs from 'fs-extra' import { v4 as uuid } from 'uuid' @@ -9,7 +10,12 @@ import { pipeline } from 'node:stream/promises' export default { Query: { async assetById(obj, args, context) { - return null + const asset = await WIKI.db.assets.query().findById(args.id) + if (asset) { + return asset + } else { + throw new Error('ERR_ASSET_NOT_FOUND') + } } }, Mutation: { @@ -18,75 +24,75 @@ export default { */ async renameAsset(obj, args, context) { try { - const filename = sanitize(args.filename).toLowerCase() + const filename = sanitize(args.fileName).toLowerCase() const asset = await WIKI.db.assets.query().findById(args.id) - if (asset) { + const treeItem = await WIKI.db.tree.query().findById(args.id) + if (asset && treeItem) { // Check for extension mismatch - if (!_.endsWith(filename, asset.ext)) { - throw new WIKI.Error.AssetRenameInvalidExt() + if (!_.endsWith(filename, asset.fileExt)) { + throw new Error('ERR_ASSET_EXT_MISMATCH') } // Check for non-dot files changing to dotfile - if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) { - throw new WIKI.Error.AssetRenameInvalid() + if (asset.fileExt.length > 0 && filename.length - asset.fileExt.length < 1) { + throw new Error('ERR_ASSET_INVALID_DOTFILE') } // Check for collision - const assetCollision = await WIKI.db.assets.query().where({ - filename, - folderId: asset.folderId + const assetCollision = await WIKI.db.tree.query().where({ + folderPath: treeItem.folderPath, + fileName: filename }).first() if (assetCollision) { - throw new WIKI.Error.AssetRenameCollision() - } - - // Get asset folder path - let hierarchy = [] - if (asset.folderId) { - hierarchy = await WIKI.db.assetFolders.getHierarchy(asset.folderId) + throw new Error('ERR_ASSET_ALREADY_EXISTS') } // Check source asset permissions - const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename + const assetSourcePath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) { - throw new WIKI.Error.AssetRenameForbidden() + throw new Error('ERR_FORBIDDEN') } // Check target asset permissions - const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename + const assetTargetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${filename}` : filename if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) { - throw new WIKI.Error.AssetRenameTargetForbidden() + throw new Error('ERR_TARGET_FORBIDDEN') } // Update filename + hash - const fileHash = '' // assetHelper.generateHash(assetTargetPath) + const itemHash = generateHash(assetTargetPath) await WIKI.db.assets.query().patch({ - filename: filename, - hash: fileHash - }).findById(args.id) - - // Delete old asset cache - await asset.deleteAssetCache() - - // Rename in Storage - await WIKI.db.storage.assetEvent({ - event: 'renamed', - asset: { - ...asset, - path: assetSourcePath, - destinationPath: assetTargetPath, - moveAuthorId: context.req.user.id, - moveAuthorName: context.req.user.name, - moveAuthorEmail: context.req.user.email - } - }) + fileName: filename + }).findById(asset.id) + + await WIKI.db.tree.query().patch({ + fileName: filename, + title: filename, + hash: itemHash + }).findById(treeItem.id) + + // TODO: Delete old asset cache + WIKI.events.outbound.emit('purgeItemCache', itemHash) + + // TODO: Rename in Storage + // await WIKI.db.storage.assetEvent({ + // event: 'renamed', + // asset: { + // ...asset, + // path: assetSourcePath, + // destinationPath: assetTargetPath, + // moveAuthorId: context.req.user.id, + // moveAuthorName: context.req.user.name, + // moveAuthorEmail: context.req.user.email + // } + // }) return { - responseResult: generateSuccess('Asset has been renamed successfully.') + operation: generateSuccess('Asset has been renamed successfully.') } } else { - throw new WIKI.Error.AssetInvalid() + throw new Error('ERR_INVALID_ASSET') } } catch (err) { return generateError(err) @@ -97,35 +103,38 @@ export default { */ async deleteAsset(obj, args, context) { try { - const asset = await WIKI.db.assets.query().findById(args.id) - if (asset) { + const treeItem = await WIKI.db.tree.query().findById(args.id) + if (treeItem) { // Check permissions - const assetPath = await asset.getAssetPath() + const assetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) { - throw new WIKI.Error.AssetDeleteForbidden() + throw new Error('ERR_FORBIDDEN') } - await WIKI.db.knex('assetData').where('id', args.id).del() - await WIKI.db.assets.query().deleteById(args.id) - await asset.deleteAssetCache() - - // Delete from Storage - await WIKI.db.storage.assetEvent({ - event: 'deleted', - asset: { - ...asset, - path: assetPath, - authorId: context.req.user.id, - authorName: context.req.user.name, - authorEmail: context.req.user.email - } - }) + // Delete from DB + await WIKI.db.assets.query().deleteById(treeItem.id) + await WIKI.db.tree.query().deleteById(treeItem.id) + + // TODO: Delete asset cache + WIKI.events.outbound.emit('purgeItemCache', treeItem.hash) + + // TODO: Delete from Storage + // await WIKI.db.storage.assetEvent({ + // event: 'deleted', + // asset: { + // ...asset, + // path: assetPath, + // authorId: context.req.user.id, + // authorName: context.req.user.name, + // authorEmail: context.req.user.email + // } + // }) return { - responseResult: generateSuccess('Asset has been deleted successfully.') + operation: generateSuccess('Asset has been deleted successfully.') } } else { - throw new WIKI.Error.AssetInvalid() + throw new Error('ERR_INVALID_ASSET') } } catch (err) { return generateError(err) @@ -373,7 +382,7 @@ export default { try { await WIKI.db.assets.flushTempUploads() return { - responseResult: generateSuccess('Temporary Uploads have been flushed successfully.') + operation: generateSuccess('Temporary Uploads have been flushed successfully.') } } catch (err) { return generateError(err) diff --git a/server/graph/schemas/asset.graphql b/server/graph/schemas/asset.graphql index 2f7a310a..b396f129 100644 --- a/server/graph/schemas/asset.graphql +++ b/server/graph/schemas/asset.graphql @@ -5,13 +5,13 @@ extend type Query { assetById( id: UUID! - ): [AssetItem] + ): AssetItem } extend type Mutation { renameAsset( id: UUID! - filename: String! + fileName: String! ): DefaultResponse deleteAsset( @@ -39,7 +39,7 @@ extend type Mutation { type AssetItem { id: UUID - filename: String + fileName: String ext: String kind: AssetKind mime: String diff --git a/server/locales/en.json b/server/locales/en.json index 6d15c861..7159d4ff 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -1643,6 +1643,13 @@ "fileman.aiFileType": "Adobe Illustrator Document", "fileman.aifFileType": "AIF Audio File", "fileman.apkFileType": "Android Package", + "fileman.assetDelete": "Delete Asset", + "fileman.assetDeleteConfirm": "Are you sure you want to delete {name}?", + "fileman.assetDeleteId": "Asset ID {id}", + "fileman.assetDeleteSuccess": "Asset deleted successfully.", + "fileman.assetFileName": "Asset Name", + "fileman.assetFileNameHint": "Filename of the asset, including the file extension.", + "fileman.assetRename": "Rename Asset", "fileman.aviFileType": "AVI Video File", "fileman.binFileType": "Binary File", "fileman.bz2FileType": "BZIP2 Archive", @@ -1698,6 +1705,8 @@ "fileman.pptxFileType": "Microsoft Powerpoint Presentation", "fileman.psdFileType": "Adobe Photoshop Document", "fileman.rarFileType": "RAR Archive", + "fileman.renameAssetInvalid": "Asset name is invalid.", + "fileman.renameAssetSuccess": "Asset renamed successfully", "fileman.renameFolderInvalidData": "One or more fields are invalid.", "fileman.renameFolderSuccess": "Folder renamed successfully.", "fileman.searchFolder": "Search folder...", diff --git a/server/models/assets.mjs b/server/models/assets.mjs index 150e5ca4..a0927c04 100644 --- a/server/models/assets.mjs +++ b/server/models/assets.mjs @@ -47,13 +47,13 @@ export class Asset extends Model { async $beforeUpdate(opt, context) { await super.$beforeUpdate(opt, context) - this.updatedAt = moment.utc().toISOString() + this.updatedAt = new Date().toISOString() } async $beforeInsert(context) { await super.$beforeInsert(context) - this.createdAt = moment.utc().toISOString() - this.updatedAt = moment.utc().toISOString() + this.createdAt = new Date().toISOString() + this.updatedAt = new Date().toISOString() } async getAssetPath() { diff --git a/server/models/pages.mjs b/server/models/pages.mjs index 2b01b7a5..e1cb8360 100644 --- a/server/models/pages.mjs +++ b/server/models/pages.mjs @@ -13,19 +13,16 @@ import CleanCSS from 'clean-css' import TurndownService from 'turndown' import { gfm as turndownPluginGfm } from '@joplin/turndown-plugin-gfm' import cheerio from 'cheerio' +import matter from 'gray-matter' -import { Locale } from './locales.mjs' import { PageLink } from './pageLinks.mjs' -import { Tag } from './tags.mjs' import { User } from './users.mjs' const pageRegex = /^[a-zA-Z0-9-_/]*$/ const aliasRegex = /^[a-zA-Z0-9-_]*$/ const frontmatterRegex = { - html: /^()?(?:\n|\r)*([\w\W]*)*/, - legacy: /^(