feat: asset rename + asset delete dialogs + linting fixes

pull/7004/head
NGPixel 11 months ago
parent f8bc9e8c24
commit 291fe26272
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -8,7 +8,7 @@
"vue" "vue"
], ],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"server/locales", "server/locales",

@ -1,6 +1,7 @@
import _ from 'lodash-es' import _ from 'lodash-es'
import sanitize from 'sanitize-filename' import sanitize from 'sanitize-filename'
import { generateError, generateSuccess } from '../../helpers/graph.mjs' import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { decodeFolderPath, decodeTreePath, generateHash } from '../../helpers/common.mjs'
import path from 'node:path' import path from 'node:path'
import fs from 'fs-extra' import fs from 'fs-extra'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
@ -9,7 +10,12 @@ import { pipeline } from 'node:stream/promises'
export default { export default {
Query: { Query: {
async assetById(obj, args, context) { 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: { Mutation: {
@ -18,75 +24,75 @@ export default {
*/ */
async renameAsset(obj, args, context) { async renameAsset(obj, args, context) {
try { try {
const filename = sanitize(args.filename).toLowerCase() const filename = sanitize(args.fileName).toLowerCase()
const asset = await WIKI.db.assets.query().findById(args.id) 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 // Check for extension mismatch
if (!_.endsWith(filename, asset.ext)) { if (!_.endsWith(filename, asset.fileExt)) {
throw new WIKI.Error.AssetRenameInvalidExt() throw new Error('ERR_ASSET_EXT_MISMATCH')
} }
// Check for non-dot files changing to dotfile // Check for non-dot files changing to dotfile
if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) { if (asset.fileExt.length > 0 && filename.length - asset.fileExt.length < 1) {
throw new WIKI.Error.AssetRenameInvalid() throw new Error('ERR_ASSET_INVALID_DOTFILE')
} }
// Check for collision // Check for collision
const assetCollision = await WIKI.db.assets.query().where({ const assetCollision = await WIKI.db.tree.query().where({
filename, folderPath: treeItem.folderPath,
folderId: asset.folderId fileName: filename
}).first() }).first()
if (assetCollision) { if (assetCollision) {
throw new WIKI.Error.AssetRenameCollision() throw new Error('ERR_ASSET_ALREADY_EXISTS')
}
// Get asset folder path
let hierarchy = []
if (asset.folderId) {
hierarchy = await WIKI.db.assetFolders.getHierarchy(asset.folderId)
} }
// Check source asset permissions // 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 })) { 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 // 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 })) { 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 // Update filename + hash
const fileHash = '' // assetHelper.generateHash(assetTargetPath) const itemHash = generateHash(assetTargetPath)
await WIKI.db.assets.query().patch({ await WIKI.db.assets.query().patch({
filename: filename, fileName: filename
hash: fileHash }).findById(asset.id)
}).findById(args.id)
await WIKI.db.tree.query().patch({
// Delete old asset cache fileName: filename,
await asset.deleteAssetCache() title: filename,
hash: itemHash
// Rename in Storage }).findById(treeItem.id)
await WIKI.db.storage.assetEvent({
event: 'renamed', // TODO: Delete old asset cache
asset: { WIKI.events.outbound.emit('purgeItemCache', itemHash)
...asset,
path: assetSourcePath, // TODO: Rename in Storage
destinationPath: assetTargetPath, // await WIKI.db.storage.assetEvent({
moveAuthorId: context.req.user.id, // event: 'renamed',
moveAuthorName: context.req.user.name, // asset: {
moveAuthorEmail: context.req.user.email // ...asset,
} // path: assetSourcePath,
}) // destinationPath: assetTargetPath,
// moveAuthorId: context.req.user.id,
// moveAuthorName: context.req.user.name,
// moveAuthorEmail: context.req.user.email
// }
// })
return { return {
responseResult: generateSuccess('Asset has been renamed successfully.') operation: generateSuccess('Asset has been renamed successfully.')
} }
} else { } else {
throw new WIKI.Error.AssetInvalid() throw new Error('ERR_INVALID_ASSET')
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)
@ -97,35 +103,38 @@ export default {
*/ */
async deleteAsset(obj, args, context) { async deleteAsset(obj, args, context) {
try { try {
const asset = await WIKI.db.assets.query().findById(args.id) const treeItem = await WIKI.db.tree.query().findById(args.id)
if (asset) { if (treeItem) {
// Check permissions // 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 })) { 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() // Delete from DB
await WIKI.db.assets.query().deleteById(args.id) await WIKI.db.assets.query().deleteById(treeItem.id)
await asset.deleteAssetCache() await WIKI.db.tree.query().deleteById(treeItem.id)
// Delete from Storage // TODO: Delete asset cache
await WIKI.db.storage.assetEvent({ WIKI.events.outbound.emit('purgeItemCache', treeItem.hash)
event: 'deleted',
asset: { // TODO: Delete from Storage
...asset, // await WIKI.db.storage.assetEvent({
path: assetPath, // event: 'deleted',
authorId: context.req.user.id, // asset: {
authorName: context.req.user.name, // ...asset,
authorEmail: context.req.user.email // path: assetPath,
} // authorId: context.req.user.id,
}) // authorName: context.req.user.name,
// authorEmail: context.req.user.email
// }
// })
return { return {
responseResult: generateSuccess('Asset has been deleted successfully.') operation: generateSuccess('Asset has been deleted successfully.')
} }
} else { } else {
throw new WIKI.Error.AssetInvalid() throw new Error('ERR_INVALID_ASSET')
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)
@ -373,7 +382,7 @@ export default {
try { try {
await WIKI.db.assets.flushTempUploads() await WIKI.db.assets.flushTempUploads()
return { return {
responseResult: generateSuccess('Temporary Uploads have been flushed successfully.') operation: generateSuccess('Temporary Uploads have been flushed successfully.')
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)

@ -5,13 +5,13 @@
extend type Query { extend type Query {
assetById( assetById(
id: UUID! id: UUID!
): [AssetItem] ): AssetItem
} }
extend type Mutation { extend type Mutation {
renameAsset( renameAsset(
id: UUID! id: UUID!
filename: String! fileName: String!
): DefaultResponse ): DefaultResponse
deleteAsset( deleteAsset(
@ -39,7 +39,7 @@ extend type Mutation {
type AssetItem { type AssetItem {
id: UUID id: UUID
filename: String fileName: String
ext: String ext: String
kind: AssetKind kind: AssetKind
mime: String mime: String

@ -1643,6 +1643,13 @@
"fileman.aiFileType": "Adobe Illustrator Document", "fileman.aiFileType": "Adobe Illustrator Document",
"fileman.aifFileType": "AIF Audio File", "fileman.aifFileType": "AIF Audio File",
"fileman.apkFileType": "Android Package", "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.aviFileType": "AVI Video File",
"fileman.binFileType": "Binary File", "fileman.binFileType": "Binary File",
"fileman.bz2FileType": "BZIP2 Archive", "fileman.bz2FileType": "BZIP2 Archive",
@ -1698,6 +1705,8 @@
"fileman.pptxFileType": "Microsoft Powerpoint Presentation", "fileman.pptxFileType": "Microsoft Powerpoint Presentation",
"fileman.psdFileType": "Adobe Photoshop Document", "fileman.psdFileType": "Adobe Photoshop Document",
"fileman.rarFileType": "RAR Archive", "fileman.rarFileType": "RAR Archive",
"fileman.renameAssetInvalid": "Asset name is invalid.",
"fileman.renameAssetSuccess": "Asset renamed successfully",
"fileman.renameFolderInvalidData": "One or more fields are invalid.", "fileman.renameFolderInvalidData": "One or more fields are invalid.",
"fileman.renameFolderSuccess": "Folder renamed successfully.", "fileman.renameFolderSuccess": "Folder renamed successfully.",
"fileman.searchFolder": "Search folder...", "fileman.searchFolder": "Search folder...",

@ -47,13 +47,13 @@ export class Asset extends Model {
async $beforeUpdate(opt, context) { async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context) await super.$beforeUpdate(opt, context)
this.updatedAt = moment.utc().toISOString() this.updatedAt = new Date().toISOString()
} }
async $beforeInsert(context) { async $beforeInsert(context) {
await super.$beforeInsert(context) await super.$beforeInsert(context)
this.createdAt = moment.utc().toISOString() this.createdAt = new Date().toISOString()
this.updatedAt = moment.utc().toISOString() this.updatedAt = new Date().toISOString()
} }
async getAssetPath() { async getAssetPath() {

@ -13,19 +13,16 @@ import CleanCSS from 'clean-css'
import TurndownService from 'turndown' import TurndownService from 'turndown'
import { gfm as turndownPluginGfm } from '@joplin/turndown-plugin-gfm' import { gfm as turndownPluginGfm } from '@joplin/turndown-plugin-gfm'
import cheerio from 'cheerio' import cheerio from 'cheerio'
import matter from 'gray-matter'
import { Locale } from './locales.mjs'
import { PageLink } from './pageLinks.mjs' import { PageLink } from './pageLinks.mjs'
import { Tag } from './tags.mjs'
import { User } from './users.mjs' import { User } from './users.mjs'
const pageRegex = /^[a-zA-Z0-9-_/]*$/ const pageRegex = /^[a-zA-Z0-9-_/]*$/
const aliasRegex = /^[a-zA-Z0-9-_]*$/ const aliasRegex = /^[a-zA-Z0-9-_]*$/
const frontmatterRegex = { const frontmatterRegex = {
html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/, html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/
legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/
} }
/** /**
@ -178,30 +175,20 @@ export class Page extends Model {
* @returns {Object} Parsed Page Metadata with Raw Content * @returns {Object} Parsed Page Metadata with Raw Content
*/ */
static parseMetadata (raw, contentType) { static parseMetadata (raw, contentType) {
let result
try { try {
switch (contentType) { switch (contentType) {
case 'markdown': case 'markdown': {
result = frontmatterRegex.markdown.exec(raw) const result = matter(raw)
if (result[2]) { if (!result?.isEmpty) {
return { return {
...yaml.safeLoad(result[2]), content: result.content,
content: result[3] ...result.data
}
} else {
// Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw)
if (result[2]) {
return {
title: result[2],
description: result[4],
content: result[5]
}
} }
} }
break break
case 'html': }
result = frontmatterRegex.html.exec(raw) case 'html': {
const result = frontmatterRegex.html.exec(raw)
if (result[2]) { if (result[2]) {
return { return {
...yaml.safeLoad(result[2]), ...yaml.safeLoad(result[2]),
@ -209,6 +196,7 @@ export class Page extends Model {
} }
} }
break break
}
} }
} catch (err) { } catch (err) {
WIKI.logger.warn('Failed to parse page metadata. Invalid syntax.') WIKI.logger.warn('Failed to parse page metadata. Invalid syntax.')

@ -85,6 +85,7 @@
"graphql-rate-limit-directive": "2.0.4", "graphql-rate-limit-directive": "2.0.4",
"graphql-tools": "9.0.0", "graphql-tools": "9.0.0",
"graphql-upload": "16.0.2", "graphql-upload": "16.0.2",
"gray-matter": "4.0.3",
"he": "1.2.0", "he": "1.2.0",
"highlight.js": "11.9.0", "highlight.js": "11.9.0",
"image-size": "1.0.2", "image-size": "1.0.2",

@ -152,6 +152,9 @@ dependencies:
graphql-upload: graphql-upload:
specifier: 16.0.2 specifier: 16.0.2
version: 16.0.2(graphql@16.8.1) version: 16.0.2(graphql@16.8.1)
gray-matter:
specifier: 4.0.3
version: 4.0.3
he: he:
specifier: 1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
@ -2114,6 +2117,12 @@ packages:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
dev: false dev: false
/argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
dev: false
/argparse@2.0.1: /argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -3852,6 +3861,13 @@ packages:
- supports-color - supports-color
dev: false dev: false
/extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
dependencies:
is-extendable: 0.1.1
dev: false
/extend@3.0.2: /extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
dev: false dev: false
@ -4290,6 +4306,16 @@ packages:
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
dev: false dev: false
/gray-matter@4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
dependencies:
js-yaml: 3.14.1
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
dev: false
/has-bigints@1.0.2: /has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true dev: true
@ -4625,6 +4651,11 @@ packages:
has-tostringtag: 1.0.0 has-tostringtag: 1.0.0
dev: true dev: true
/is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
dev: false
/is-extglob@2.1.1: /is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4762,6 +4793,14 @@ packages:
dev: false dev: false
optional: true optional: true
/js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
dependencies:
argparse: 1.0.10
esprima: 4.0.1
dev: false
/js-yaml@4.1.0: /js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
@ -4895,6 +4934,11 @@ packages:
json-buffer: 3.0.1 json-buffer: 3.0.1
dev: true dev: true
/kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
dev: false
/klaw@4.1.0: /klaw@4.1.0:
resolution: {integrity: sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==} resolution: {integrity: sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==}
engines: {node: '>=14.14.0'} engines: {node: '>=14.14.0'}
@ -6591,6 +6635,14 @@ packages:
apg-lib: 3.2.0 apg-lib: 3.2.0
dev: false dev: false
/section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
dev: false
/semver@6.3.1: /semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@ -6821,6 +6873,10 @@ packages:
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
dev: false dev: false
/sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: false
/statuses@2.0.1: /statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -6890,6 +6946,11 @@ packages:
dependencies: dependencies:
ansi-regex: 5.0.1 ansi-regex: 5.0.1
/strip-bom-string@1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
dev: false
/strip-bom@3.0.0: /strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'} engines: {node: '>=4'}

@ -0,0 +1,31 @@
---
title: Home
description: Welcome to your wiki!
published: true
---
Feel free to modify this page (or delete it!).
## Next Steps
- Configure your wiki in the [Administration Area](/_admin).
- [Modify your profile](/_profile) to set preferences or change your password.
- Create new pages by clicking the <kbd>+</kbd> button in the upper right corner.
- Edit the navigation by clicking the <kbd>Edit Nav</kbd> button in the lower left corner.
## Read the documentation
How do permissions work? How can I make my wiki publicly accessible?
It's all [in the docs](https://beta.js.wiki/docs/admin/groups)!
## Example Blocks
Did you know that you can insert dynamic [blocks](https://beta.js.wiki/docs/editor/blocks)?
For example, here are the 5 most recently updated pages on your wiki:
::block-index{orderBy="updatedAt" orderByDirection="desc" limit="5"}
::
This list will automatically update as you create / edit pages.

@ -26,6 +26,8 @@ module.exports = {
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
'plugin:vue-pug/vue3-strongly-recommended',
'standard' 'standard'
], ],
@ -72,6 +74,14 @@ module.exports = {
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
// allow debugger during development only // allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// disable bogus rules
'vue/valid-template-root': 'off',
'vue/no-parsing-error': 'off',
'vue-pug/no-parsing-error': 'off',
'vue/valid-v-for': 'off',
'vue/html-quotes': ['warn', 'single'],
'vue/max-attributes-per-line': 'off'
} }
} }

@ -40,7 +40,7 @@
"vueCompilerOptions": { "vueCompilerOptions": {
"target": 3, "target": 3,
"plugins": [ "plugins": [
"@volar/vue-language-plugin-pug" "@vue/language-plugin-pug"
] ]
} }
} }

@ -117,7 +117,8 @@
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.29.0",
"eslint-plugin-n": "16.3.1", "eslint-plugin-n": "16.3.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.18.1" "eslint-plugin-vue": "9.18.1",
"eslint-plugin-vue-pug": "0.6.1"
}, },
"engines": { "engines": {
"node": ">= 18.0", "node": ">= 18.0",

@ -325,6 +325,9 @@ devDependencies:
eslint-plugin-vue: eslint-plugin-vue:
specifier: 9.18.1 specifier: 9.18.1
version: 9.18.1(eslint@8.54.0) version: 9.18.1(eslint@8.54.0)
eslint-plugin-vue-pug:
specifier: 0.6.1
version: 0.6.1(eslint-plugin-vue@9.18.1)(vue-eslint-parser@9.3.2)
packages: packages:
@ -2736,6 +2739,17 @@ packages:
eslint: 8.54.0 eslint: 8.54.0
dev: true dev: true
/eslint-plugin-vue-pug@0.6.1(eslint-plugin-vue@9.18.1)(vue-eslint-parser@9.3.2):
resolution: {integrity: sha512-wOId81xH42+X9M0qVRU5o39KJ3Phd+fdgemPNAy1cD9hUROp/aSHHapOP7muDV/sHmu9zP/mU34yDfCfQWUWEQ==}
peerDependencies:
eslint-plugin-vue: ^9.8.0
dependencies:
eslint-plugin-vue: 9.18.1(eslint@8.54.0)
vue-eslint-parser-template-tokenizer-pug: 0.4.10(vue-eslint-parser@9.3.2)
transitivePeerDependencies:
- vue-eslint-parser
dev: true
/eslint-plugin-vue@9.18.1(eslint@8.54.0): /eslint-plugin-vue@9.18.1(eslint@8.54.0):
resolution: {integrity: sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==} resolution: {integrity: sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==}
engines: {node: ^14.17.0 || >=16.0.0} engines: {node: ^14.17.0 || >=16.0.0}
@ -5478,6 +5492,15 @@ packages:
dependencies: dependencies:
vue: 3.3.8(typescript@5.3.2) vue: 3.3.8(typescript@5.3.2)
/vue-eslint-parser-template-tokenizer-pug@0.4.10(vue-eslint-parser@9.3.2):
resolution: {integrity: sha512-Npzjna9PUJzIal/o7hOo4D7dF4hqjHwTafBtLgdtja2LZuCc4UT5BU7dyYJeKb9s1SnCFBflHMg3eFA3odq6bg==}
peerDependencies:
vue-eslint-parser: ^9.0.0
dependencies:
pug-lexer: 5.0.1
vue-eslint-parser: 9.3.2(eslint@8.54.0)
dev: true
/vue-eslint-parser@9.3.2(eslint@8.54.0): /vue-eslint-parser@9.3.2(eslint@8.54.0):
resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==}
engines: {node: ^14.17.0 || >=16.0.0} engines: {node: ^14.17.0 || >=16.0.0}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M1.5 4.5H38.5V35.5H1.5z"/><path fill="#4788c7" d="M38,5v30H2V5H38 M39,4H1v32h38V4L39,4z"/><path fill="#98ccfd" d="M27.45 19.612L20 25.437 30.247 35 38 35 38 29.112zM30 10A3 3 0 1 0 30 16 3 3 0 1 0 30 10z"/><path fill="#b6dcfe" d="M32.468 35L2 35 2 27.421 14 17.316z"/><g><path fill="#fff" d="M36,7v26H4V7H36 M38,5H2v30h36V5L38,5z"/></g></svg>

After

Width:  |  Height:  |  Size: 454 B

@ -5,7 +5,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide', persistent)
q-icon(name='img:/_assets/icons/fluent-key-2.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-key-2.svg', left, size='sm')
span {{t(`admin.api.copyKeyTitle`)}} span {{t(`admin.api.copyKeyTitle`)}}
q-card-section.card-negative q-card-section.card-negative
i18n-t(tag='span', keypath='admin.api.newKeyCopyWarn') i18n-t(tag='span', keypath='admin.api.newKeyCopyWarn', scope='global')
template(#bold) template(#bold)
strong {{t('admin.api.newKeyCopyWarnBold')}} strong {{t('admin.api.newKeyCopyWarnBold')}}
q-form.q-py-sm q-form.q-py-sm
@ -15,7 +15,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide', persistent)
q-input( q-input(
type='textarea' type='textarea'
outlined outlined
v-model='props.keyValue' :model-value='props.keyValue'
dense dense
hide-bottom-space hide-bottom-space
:label='t(`admin.api.key`)' :label='t(`admin.api.key`)'

@ -64,11 +64,11 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
) )
template(v-slot:selected) template(v-slot:selected)
.text-caption(v-if='state.keyGroups.length > 1') .text-caption(v-if='state.keyGroups.length > 1')
i18n-t(keypath='admin.api.groupsSelected') i18n-t(keypath='admin.api.groupsSelected', scope='global')
template(#count) template(#count)
strong {{ state.keyGroups.length }} strong {{ state.keyGroups.length }}
.text-caption(v-else-if='state.keyGroups.length === 1') .text-caption(v-else-if='state.keyGroups.length === 1')
i18n-t(keypath='admin.api.groupSelected') i18n-t(keypath='admin.api.groupSelected', scope='global')
template(#group) template(#group)
strong {{ selectedGroupName }} strong {{ selectedGroupName }}
span(v-else) span(v-else)

@ -0,0 +1,109 @@
<template lang='pug'>
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 550px; max-width: 850px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span {{ t(`fileman.assetDelete`) }}
q-card-section
.text-body2
i18n-t(keypath='fileman.assetDeleteConfirm')
template(#name)
strong {{assetName}}
.text-caption.text-grey.q-mt-sm {{ t('fileman.assetDeleteId', { id: assetId }) }}
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.delete`)'
color='negative'
padding='xs md'
@click='confirm'
:loading='state.isLoading'
)
</template>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
// PROPS
const props = defineProps({
assetId: {
type: String,
required: true
},
assetName: {
type: String,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deleteAsset ($id: UUID!) {
deleteAsset(id: $id) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: props.assetId
}
})
if (resp?.data?.deleteAsset?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('fileman.assetDeleteSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteAsset?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>

@ -0,0 +1,165 @@
<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.assetRename`) }}
q-form.q-py-sm(@submit='rename')
q-item
blueprint-icon.self-start(icon='image')
q-item-section
q-input(
autofocus
outlined
v-model='state.path'
dense
hide-bottom-space
:label='t(`fileman.assetFileName`)'
:aria-label='t(`fileman.assetFileName`)'
:hint='t(`fileman.assetFileNameHint`)'
lazy-rules='ondemand'
@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 } from 'vue'
// PROPS
const props = defineProps({
assetId: {
type: String,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
path: '',
loading: false
})
// METHODS
async function rename () {
state.loading++
try {
if (state.path?.length < 2 || !state.path?.includes('.')) {
throw new Error(t('fileman.renameAssetInvalid'))
}
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation renameAsset (
$id: UUID!
$fileName: String!
) {
renameAsset (
id: $id
fileName: $fileName
) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: props.assetId,
fileName: state.path
}
})
if (resp?.data?.renameAsset?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('fileman.renameAssetSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.renameAsset?.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 fetchAssetForRename (
$id: UUID!
) {
assetById (
id: $id
) {
id
fileName
}
}
`,
fetchPolicy: 'network-only',
variables: {
id: props.assetId
}
})
if (resp?.data?.assetById?.id !== props.assetId) {
throw new Error('Failed to fetch asset data.')
}
state.path = resp.data.assetById.fileName
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
onDialogCancel()
}
state.loading--
})
</script>

@ -1,9 +1,9 @@
<template lang="pug"> <template lang='pug'>
q-layout.fileman(view='hHh lpR lFr', container) q-layout.fileman(view='hHh lpR lFr', container)
q-header.card-header q-header.card-header
q-toolbar(dark) q-toolbar(dark)
q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md') q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md')
span {{t(`fileman.title`)}} span {{ t(`fileman.title`) }}
q-toolbar(dark) q-toolbar(dark)
q-btn.q-mr-sm.acrylic-btn( q-btn.q-mr-sm.acrylic-btn(
flat flat
@ -23,9 +23,9 @@ q-layout.fileman(view='hHh lpR lFr', container)
:label='t(`fileman.searchFolder`)' :label='t(`fileman.searchFolder`)'
:debounce='500' :debounce='500'
) )
template(v-slot:prepend) template(#prepend)
q-icon(name='las la-search') q-icon(name='las la-search')
template(v-slot:append) template(#append)
q-icon.cursor-pointer( q-icon.cursor-pointer(
name='las la-times' name='las la-times'
@click='state.search=``' @click='state.search=``'
@ -78,9 +78,10 @@ q-layout.fileman(view='hHh lpR lFr', container)
) )
.fileman-details-row( .fileman-details-row(
v-for='item of currentFileDetails.items' v-for='item of currentFileDetails.items'
:key='item.id'
) )
label {{item.label}} label {{ item.label }}
span {{item.value}} span {{ item.value }}
template(v-if='insertMode') template(v-if='insertMode')
q-separator.q-my-md q-separator.q-my-md
q-btn.full-width( q-btn.full-width(
@ -97,7 +98,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-toolbar.fileman-toolbar q-toolbar.fileman-toolbar
template(v-if='state.isUploading') template(v-if='state.isUploading')
.fileman-progressbar .fileman-progressbar
div(:style='`width: ` + state.uploadPercentage + `%`') {{state.uploadPercentage}}% div(:style='`width: ` + state.uploadPercentage + `%`') {{ state.uploadPercentage }}%
q-btn.acrylic-btn.q-ml-sm( q-btn.acrylic-btn.q-ml-sm(
flat flat
dense dense
@ -117,9 +118,8 @@ q-layout.fileman(view='hHh lpR lFr', container)
color='grey' color='grey'
:aria-label='t(`fileman.viewOptions`)' :aria-label='t(`fileman.viewOptions`)'
icon='las la-th-list' icon='las la-th-list'
@click=''
) )
q-tooltip(anchor='bottom middle', self='top middle') {{t(`fileman.viewOptions`)}} q-tooltip(anchor='bottom middle', self='top middle') {{ t(`fileman.viewOptions`) }}
q-menu( q-menu(
transition-show='jump-down' transition-show='jump-down'
transition-hide='jump-up' transition-hide='jump-up'
@ -128,7 +128,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
) )
q-card.q-pa-sm q-card.q-pa-sm
.text-center .text-center
small.text-grey {{t(`fileman.viewOptions`)}} small.text-grey {{ t(`fileman.viewOptions`) }}
q-list(dense) q-list(dense)
q-separator.q-my-sm q-separator.q-my-sm
q-item(clickable) q-item(clickable)
@ -183,7 +183,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
icon='las la-redo-alt' icon='las la-redo-alt'
@click='reloadFolder(state.currentFolderId)' @click='reloadFolder(state.currentFolderId)'
) )
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.refresh`)}} q-tooltip(anchor='bottom middle', self='top middle') {{ t(`common.actions.refresh`) }}
q-separator.q-mr-sm(inset, vertical) q-separator.q-mr-sm(inset, vertical)
q-btn.q-mr-sm( q-btn.q-mr-sm(
flat flat
@ -193,7 +193,6 @@ q-layout.fileman(view='hHh lpR lFr', container)
:label='t(`common.actions.new`)' :label='t(`common.actions.new`)'
:aria-label='t(`common.actions.new`)' :aria-label='t(`common.actions.new`)'
icon='las la-plus-circle' icon='las la-plus-circle'
@click=''
) )
new-menu( new-menu(
:hide-asset-btn='true' :hide-asset-btn='true'
@ -236,16 +235,16 @@ q-layout.fileman(view='hHh lpR lFr', container)
clickable clickable
active-class='active' active-class='active'
:active='item.id === state.currentFileId' :active='item.id === state.currentFileId'
@click.native='selectItem(item)' @click='selectItem(item)'
@dblclick.native='doubleClickItem(item)' @dblclick='doubleClickItem(item)'
) )
q-item-section.fileman-filelist-icon(avatar) q-item-section.fileman-filelist-icon(avatar)
q-icon(:name='item.icon', :size='state.isCompact ? `md` : `xl`') q-icon(:name='item.icon', :size='state.isCompact ? `md` : `xl`')
q-item-section.fileman-filelist-label q-item-section.fileman-filelist-label
q-item-label {{usePathTitle ? item.fileName : item.title}} q-item-label {{ usePathTitle ? item.fileName : item.title }}
q-item-label(caption, v-if='!state.isCompact') {{item.caption}} q-item-label(caption, v-if='!state.isCompact') {{ item.caption }}
q-item-section.fileman-filelist-side(side, v-if='item.side') q-item-section.fileman-filelist-side(side, v-if='item.side')
.text-caption {{item.side}} .text-caption {{ item.side }}
//- RIGHT-CLICK MENU //- RIGHT-CLICK MENU
q-menu.translucent-menu( q-menu.translucent-menu(
touch-position touch-position
@ -341,6 +340,7 @@ import { useSiteStore } from 'src/stores/site'
import FolderCreateDialog from 'src/components/FolderCreateDialog.vue' import FolderCreateDialog from 'src/components/FolderCreateDialog.vue'
import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue' import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue'
import FolderRenameDialog from 'src/components/FolderRenameDialog.vue' import FolderRenameDialog from 'src/components/FolderRenameDialog.vue'
import AssetRenameDialog from 'src/components/AssetRenameDialog.vue'
import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue' import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue'
// QUASAR // QUASAR
@ -787,6 +787,7 @@ function reloadFolder (folderId) {
treeComp.value.resetLoaded() treeComp.value.resetLoaded()
} }
// --------------------------------------
// PAGE METHODS // PAGE METHODS
// -------------------------------------- // --------------------------------------
@ -811,6 +812,34 @@ function delPage (pageId, pageName) {
}) })
} }
// --------------------------------------
// ASSET METHODS
// --------------------------------------
function renameAsset (assetId) {
$q.dialog({
component: AssetRenameDialog,
componentProps: {
assetId
}
}).onOk(async () => {
// -> Reload current view
await loadTree({ parentId: state.currentFolderId })
})
}
function delAsset (assetId, assetName) {
$q.dialog({
component: defineAsyncComponent(() => import('src/components/AssetDeleteDialog.vue')),
componentProps: {
assetId,
assetName
}
}).onOk(() => {
loadTree(state.currentFolderId, null)
})
}
// -------------------------------------- // --------------------------------------
// UPLOAD METHODS // UPLOAD METHODS
// -------------------------------------- // --------------------------------------
@ -991,7 +1020,7 @@ function renameItem (item) {
break break
} }
case 'asset': { case 'asset': {
// TODO: Rename asset renameAsset(item.id)
break break
} }
} }
@ -999,6 +1028,10 @@ function renameItem (item) {
function delItem (item) { function delItem (item) {
switch (item.type) { switch (item.type) {
case 'asset': {
delAsset(item.id, item.title)
break
}
case 'folder': { case 'folder': {
delFolder(item.id, true) delFolder(item.id, true)
break break

@ -5,6 +5,7 @@ q-footer.site-footer
v-if='hasSiteFooter' v-if='hasSiteFooter'
:keypath='isCopyright ? `common.footerCopyright` : `common.footerLicense`' :keypath='isCopyright ? `common.footerCopyright` : `common.footerLicense`'
tag='span' tag='span'
scope='global'
) )
template(#company) template(#company)
strong {{siteStore.company}} strong {{siteStore.company}}
@ -15,6 +16,7 @@ q-footer.site-footer
i18n-t( i18n-t(
:keypath='props.generic ? `common.footerGeneric` : `common.footerPoweredBy`' :keypath='props.generic ? `common.footerGeneric` : `common.footerPoweredBy`'
tag='span' tag='span'
scope='global'
) )
template(#link) template(#link)
a(href='https://js.wiki', target='_blank', ref='noopener noreferrer'): strong Wiki.js a(href='https://js.wiki', target='_blank', ref='noopener noreferrer'): strong Wiki.js

@ -66,6 +66,7 @@ q-toolbar(
.flex.q-mb-md .flex.q-mb-md
q-chip( q-chip(
v-for='tag of popularTags' v-for='tag of popularTags'
:key='tag'
square square
color='grey-8' color='grey-8'
text-color='white' text-color='white'

@ -1,5 +1,6 @@
<!-- eslint-disable -->
<template lang="pug"> <template lang="pug">
q-card.icon-picker(flat, style='width: 400px;') q-card.icon-picker(flat, style='width: 400px')
q-tabs.text-primary( q-tabs.text-primary(
v-model='state.currentTab' v-model='state.currentTab'
no-caps no-caps
@ -43,7 +44,8 @@ q-card.icon-picker(flat, style='width: 400px;')
) )
q-item-section q-item-section
q-item-label {{scope.opt.name}} 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-item-section(side, v-if='scope.opt.subset')
q-chip( q-chip(
color='primary' color='primary'

@ -8,6 +8,7 @@ q-menu.translucent-menu(
q-list(padding, style='min-width: 200px;') q-list(padding, style='min-width: 200px;')
q-item( q-item(
v-for='lang of siteStore.locales.active' v-for='lang of siteStore.locales.active'
:key='lang.code'
clickable clickable
@click='commonStore.setLocale(lang.code)' @click='commonStore.setLocale(lang.code)'
) )

@ -27,6 +27,7 @@ q-scroll-area.sidebar-nav(
q-item( q-item(
v-for='itemChild of item.children' v-for='itemChild of item.children'
:to='itemChild.target' :to='itemChild.target'
:key='itemChild.id'
) )
q-item-section(side) q-item-section(side)
q-icon(:name='itemChild.icon', color='white') q-icon(:name='itemChild.icon', color='white')

@ -48,7 +48,10 @@
span Pending Asset Uploads span Pending Asset Uploads
q-card-section(v-if='!hasPendingAssets') There are no assets pending uploads. q-card-section(v-if='!hasPendingAssets') There are no assets pending uploads.
q-list(v-else, separator) 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-item-section(side)
q-icon(name='las la-file-image') q-icon(name='las la-file-image')
q-item-section {{ item.fileName }} q-item-section {{ item.fileName }}

@ -38,7 +38,8 @@
q-item-section(side) q-item-section(side)
q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)', size='sm') q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)', size='sm')
q-item-section q-item-section
q-item-label(v-html='scope.opt') q-item-label
span(v-html='scope.opt')
</template> </template>
<script setup> <script setup>

@ -7,7 +7,7 @@ q-menu(
@before-hide='menuHidden' @before-hide='menuHidden'
) )
q-list(dense, padding) q-list(dense, padding)
q-item(clickable, @click='', ref='copyUrlButton') q-item(clickable, ref='copyUrlButton')
q-item-section.items-center(avatar) q-item-section.items-center(avatar)
q-icon(color='grey', name='las la-clipboard', size='sm') q-icon(color='grey', name='las la-clipboard', size='sm')
q-item-section.q-pr-md Copy URL q-item-section.q-pr-md Copy URL

@ -1,15 +1,15 @@
<template lang="pug"> <template lang='pug'>
q-dialog(ref='dialogRef', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card.page-save-dialog(style='width: 860px; max-width: 90vw;') q-card.page-save-dialog(style='width: 860px; max-width: 90vw;')
q-card-section.card-header(v-if='props.mode === `savePage`') q-card-section.card-header(v-if='props.mode === `savePage`')
q-icon(name='img:/_assets/icons/fluent-save-as.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-save-as.svg', left, size='sm')
span {{t('pageSaveDialog.title')}} span {{ t('pageSaveDialog.title') }}
q-card-section.card-header(v-else-if='props.mode === `duplicatePage`') q-card-section.card-header(v-else-if='props.mode === `duplicatePage`')
q-icon(name='img:/_assets/icons/color-documents.svg', left, size='sm') q-icon(name='img:/_assets/icons/color-documents.svg', left, size='sm')
span {{t('pageDuplicateDialog.title')}} span {{ t('pageDuplicateDialog.title') }}
q-card-section.card-header(v-else-if='props.mode === `renamePage`') q-card-section.card-header(v-else-if='props.mode === `renamePage`')
q-icon(name='img:/_assets/icons/fluent-rename.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-rename.svg', left, size='sm')
span {{t('pageRenameDialog.title')}} span {{ t('pageRenameDialog.title') }}
.row.page-save-dialog-browser .row.page-save-dialog-browser
.col-4 .col-4
q-scroll-area( q-scroll-area(
@ -36,13 +36,13 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
clickable clickable
active-class='active' active-class='active'
:active='item.id === state.currentFileId' :active='item.id === state.currentFileId'
@click.native='selectItem(item)' @click='selectItem(item)'
) )
q-item-section(side) q-item-section(side)
q-icon(:name='item.icon', size='sm') q-icon(:name='item.icon', size='sm')
q-item-section q-item-section
q-item-label {{item.title}} q-item-label {{item.title}}
.page-save-dialog-path.font-robotomono {{currentFolderPath}} .page-save-dialog-path.font-robotomono {{ currentFolderPath }}
q-list.q-py-sm q-list.q-py-sm
q-item q-item
blueprint-icon(icon='new-document') blueprint-icon(icon='new-document')

@ -28,6 +28,7 @@ q-layout.admin(view='hHh Lpr lff')
q-list(separator, padding) q-list(separator, padding)
q-item( q-item(
v-for='lang of adminStore.locales' v-for='lang of adminStore.locales'
:key='lang.code'
clickable clickable
@click='commonStore.setLocale(lang.code)' @click='commonStore.setLocale(lang.code)'
) )

@ -13,7 +13,6 @@ q-layout(view='hHh Lpr lff')
icon='las la-globe' icon='las la-globe'
color='white' color='white'
aria-label='Switch Locale' aria-label='Switch Locale'
@click=''
) )
locale-selector-menu(anchor='top right' self='top left') locale-selector-menu(anchor='top right' self='top left')
q-tooltip(anchor='center right' self='center left') Switch Locale q-tooltip(anchor='center right' self='center left') Switch Locale

@ -32,7 +32,7 @@ q-page.admin-extensions
q-card q-card
q-list(separator) q-list(separator)
q-item( q-item(
v-for='(ext, idx) of state.extensions' v-for='ext of state.extensions'
:key='`ext-` + ext.key' :key='`ext-` + ext.key'
) )
blueprint-icon(icon='module') blueprint-icon(icon='module')

@ -101,7 +101,6 @@ q-page.admin-flags
icon='las la-code' icon='las la-code'
color='primary' color='primary'
text-color='white' text-color='white'
@click=''
disabled disabled
) )

@ -84,7 +84,7 @@ q-page.admin-locale
.text-caption(:class='$q.dark.isActive ? `text-grey-4` : `text-grey-7`') Select the locales that can be used on this site. .text-caption(:class='$q.dark.isActive ? `text-grey-4` : `text-grey-7`') Select the locales that can be used on this site.
q-item( q-item(
v-for='(lc, idx) of state.locales' v-for='lc of state.locales'
:key='lc.code' :key='lc.code'
:tag='lc.code !== state.selectedLocale ? `label` : null' :tag='lc.code !== state.selectedLocale ? `label` : null'
) )

@ -177,7 +177,6 @@ q-page.admin-login
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm') q-icon(name='las la-info-circle', size='sm')
q-card-section.text-caption {{ t('admin.login.providersVisbleWarning') }} q-card-section.text-caption {{ t('admin.login.providersVisbleWarning') }}
</template> </template>
<script setup> <script setup>

@ -54,7 +54,7 @@ q-page.admin-api
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm') q-icon(name='las la-info-circle', size='sm')
q-card-section q-card-section
i18n-t(tag='span', keypath='admin.metrics.endpoint') i18n-t(tag='span', keypath='admin.metrics.endpoint', scope='global')
template(#endpoint) template(#endpoint)
strong.font-robotomono /metrics strong.font-robotomono /metrics
.text-caption {{ t('admin.metrics.endpointWarning') }} .text-caption {{ t('admin.metrics.endpointWarning') }}
@ -66,7 +66,7 @@ q-page.admin-api
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-key', size='sm') q-icon(name='las la-key', size='sm')
q-card-section q-card-section
i18n-t(tag='span', keypath='admin.metrics.auth') i18n-t(tag='span', keypath='admin.metrics.auth', scope='global')
template(#headerName) template(#headerName)
strong.font-robotomono Authorization strong.font-robotomono Authorization
template(#tokenType) template(#tokenType)

@ -32,7 +32,6 @@ q-page.admin-system
icon='mdi-clipboard-text-outline' icon='mdi-clipboard-text-outline'
label='Copy System Info' label='Copy System Info'
color='primary' color='primary'
@click=''
:disabled='state.loading > 0' :disabled='state.loading > 0'
) )
q-separator(inset) q-separator(inset)

@ -65,7 +65,7 @@ q-page.admin-theme
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='t(`admin.theme.darkMode`)' :aria-label='t(`admin.theme.darkMode`)'
) )
template(v-for='(cl, idx) of colorKeys', :key='cl') template(v-for='cl of colorKeys', :key='cl')
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='fill-color') blueprint-icon(icon='fill-color')

@ -39,7 +39,6 @@ q-page.admin-groups
unelevated unelevated
color='secondary' color='secondary'
:aria-label='t(`admin.users.defaults`)' :aria-label='t(`admin.users.defaults`)'
@click=''
) )
q-tooltip {{ t(`admin.users.defaults`) }} q-tooltip {{ t(`admin.users.defaults`) }}
user-defaults-menu user-defaults-menu

@ -44,7 +44,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
q-item q-item
@ -57,7 +56,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
q-item q-item
@ -70,7 +68,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
q-item q-item
@ -83,7 +80,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
q-item q-item
@ -108,7 +104,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
q-item q-item
@ -121,7 +116,6 @@ q-page.admin-utilities
flat flat
icon='las la-arrow-circle-right' icon='las la-arrow-circle-right'
color='primary' color='primary'
@click=''
:label='t(`common.actions.proceed`)' :label='t(`common.actions.proceed`)'
) )
</template> </template>

@ -390,7 +390,7 @@ function refreshTocExpanded (baseToc, lvl) {
} }
} }
.page-header { .page-header {
min-height: 95px; height: 95px;
@at-root .body--light & { @at-root .body--light & {
background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%); background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);

@ -67,7 +67,8 @@ q-layout(view='hHh Lpr lff')
q-item-section(side) q-item-section(side)
q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)', size='sm') q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)', size='sm')
q-item-section q-item-section
q-item-label(v-html='scope.opt') q-item-label
span(v-html='scope.opt')
//- q-input.q-mt-sm( //- q-input.q-mt-sm(
//- outlined //- outlined
//- dense //- dense
@ -103,7 +104,8 @@ q-layout(view='hHh Lpr lff')
q-item-section(side) q-item-section(side)
q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)') q-checkbox(:model-value='scope.selected', @update:model-value='scope.toggleOption(scope.opt)')
q-item-section q-item-section
q-item-label(v-html='scope.opt.name') q-item-label
span(v-html='scope.opt.name')
q-select.q-mt-sm( q-select.q-mt-sm(
outlined outlined
v-model='state.params.filterEditor' v-model='state.params.filterEditor'
@ -154,7 +156,8 @@ q-layout(view='hHh Lpr lff')
q-item-section q-item-section
q-item-label {{ item.title }} q-item-label {{ item.title }}
q-item-label(v-if='item.description', caption) {{ item.description }} q-item-label(v-if='item.description', caption) {{ item.description }}
q-item-label.text-highlight(v-if='item.highlight', caption, v-html='item.highlight') q-item-label.text-highlight(v-if='item.highlight', caption)
span(v-html='item.highlight')
q-item-section(side) q-item-section(side)
.flex.layout-search-itemtags .flex.layout-search-itemtags
q-chip( q-chip(

@ -64,7 +64,7 @@ export const useSiteStore = defineStore('site', {
}, },
sideDialogShown: false, sideDialogShown: false,
sideDialogComponent: '', sideDialogComponent: '',
docsBase: 'https://next.js.wiki/docs', docsBase: 'https://beta.js.wiki/docs',
nav: { nav: {
currentId: null, currentId: null,
items: [] items: []

Loading…
Cancel
Save