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)
+ fileName: filename
+ }).findById(asset.id)
- // Delete old asset cache
- await asset.deleteAssetCache()
+ await WIKI.db.tree.query().patch({
+ fileName: filename,
+ title: filename,
+ hash: itemHash
+ }).findById(treeItem.id)
- // 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
- }
- })
+ // 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 DB
+ await WIKI.db.assets.query().deleteById(treeItem.id)
+ await WIKI.db.tree.query().deleteById(treeItem.id)
- // 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
- }
- })
+ // 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: /^(
-q-card.icon-picker(flat, style='width: 400px;')
+q-card.icon-picker(flat, style='width: 400px')
q-tabs.text-primary(
v-model='state.currentTab'
no-caps
@@ -43,7 +44,8 @@ q-card.icon-picker(flat, style='width: 400px;')
)
q-item-section
q-item-label {{scope.opt.name}}
- q-item-label(caption): strong(:class='scope.selected ? `text-white` : `text-primary`') {{scope.opt.subset}}
+ q-item-label(caption)
+ strong(:class='scope.selected ? `text-white` : `text-primary`') {{scope.opt.subset}}
q-item-section(side, v-if='scope.opt.subset')
q-chip(
color='primary'
diff --git a/ux/src/components/LocaleSelectorMenu.vue b/ux/src/components/LocaleSelectorMenu.vue
index e6e25862..926a54b1 100644
--- a/ux/src/components/LocaleSelectorMenu.vue
+++ b/ux/src/components/LocaleSelectorMenu.vue
@@ -8,6 +8,7 @@ q-menu.translucent-menu(
q-list(padding, style='min-width: 200px;')
q-item(
v-for='lang of siteStore.locales.active'
+ :key='lang.code'
clickable
@click='commonStore.setLocale(lang.code)'
)
diff --git a/ux/src/components/NavSidebar.vue b/ux/src/components/NavSidebar.vue
index bdeda93b..12a03232 100644
--- a/ux/src/components/NavSidebar.vue
+++ b/ux/src/components/NavSidebar.vue
@@ -27,6 +27,7 @@ q-scroll-area.sidebar-nav(
q-item(
v-for='itemChild of item.children'
:to='itemChild.target'
+ :key='itemChild.id'
)
q-item-section(side)
q-icon(:name='itemChild.icon', color='white')
diff --git a/ux/src/components/PageActionsCol.vue b/ux/src/components/PageActionsCol.vue
index 032434d4..494f867a 100644
--- a/ux/src/components/PageActionsCol.vue
+++ b/ux/src/components/PageActionsCol.vue
@@ -48,7 +48,10 @@
span Pending Asset Uploads
q-card-section(v-if='!hasPendingAssets') There are no assets pending uploads.
q-list(v-else, separator)
- q-item(v-for='item of editorStore.pendingAssets')
+ q-item(
+ v-for='item of editorStore.pendingAssets'
+ :key='item.id'
+ )
q-item-section(side)
q-icon(name='las la-file-image')
q-item-section {{ item.fileName }}
diff --git a/ux/src/components/PageTags.vue b/ux/src/components/PageTags.vue
index c2ad42e5..ce56fd94 100644
--- a/ux/src/components/PageTags.vue
+++ b/ux/src/components/PageTags.vue
@@ -38,7 +38,8 @@
q-item-section(side)
q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)', size='sm')
q-item-section
- q-item-label(v-html='scope.opt')
+ q-item-label
+ span(v-html='scope.opt')