feat: editor pending asset uploads (wip)

pull/6775/head
NGPixel 1 year ago
parent 607b8d8100
commit 5a8d95ee0c
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -66,7 +66,7 @@ The current stable release (2.x) is available at https://js.wiki
```
1. In the left-side terminal (Server), run the command:
```sh
node run start
npm run start
```
1. Open your browser to `http://localhost:3000`
1. Login using the default administrator user:

@ -1,5 +1,5 @@
import express from 'express'
// import pageHelper from '../helpers/page.mjs'
import { parsePath } from '../helpers/page.mjs'
// import CleanCSS from 'clean-css'
import path from 'node:path'
@ -526,8 +526,22 @@ export default function () {
// }
// })
router.get('/*', (req, res, next) => {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
router.get('/*', async (req, res, next) => {
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
const stripExt = site.config.pageExtensions.some(ext => req.path.endsWith(`.${ext}`))
const pathArgs = parsePath(req.path, { stripExt })
const isPage = (stripExt || pathArgs.path.indexOf('.') === -1)
if (isPage) {
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
} else {
await WIKI.db.assets.getAsset({ pathArgs, siteId: site.id }, res)
}
})
return router

@ -1,5 +1,5 @@
import { Model } from 'objection'
import path from 'path'
import path from 'node:path'
import fse from 'fs-extra'
import { startsWith } from 'lodash-es'
import { generateHash } from '../helpers/common.mjs'
@ -166,24 +166,24 @@ export class Asset extends Model {
.first()
}
static async getAsset({ path, locale, siteId }, res) {
static async getAsset({ pathArgs, siteId }, res) {
try {
const fileInfo = '' // assetHelper.getPathInfo(assetPath)
const fileHash = '' // assetHelper.generateHash(assetPath)
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)
const fileInfo = path.parse(pathArgs.path.toLowerCase())
const fileHash = generateHash(pathArgs.path)
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${siteId}/${fileHash}.dat`)
// Force unsafe extensions to download
if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
if (WIKI.config.security.forceAssetDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
}
if (await WIKI.db.assets.getAssetFromCache(assetPath, cachePath, res)) {
if (await WIKI.db.assets.getAssetFromCache({ cachePath, extName: fileInfo.ext }, res)) {
return
}
if (await WIKI.db.assets.getAssetFromStorage(assetPath, res)) {
return
}
await WIKI.db.assets.getAssetFromDb(assetPath, fileHash, cachePath, res)
// if (await WIKI.db.assets.getAssetFromStorage(assetPath, res)) {
// return
// }
await WIKI.db.assets.getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res)
} catch (err) {
if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
return
@ -193,13 +193,13 @@ export class Asset extends Model {
}
}
static async getAssetFromCache(assetPath, cachePath, res) {
static async getAssetFromCache({ cachePath, extName }, res) {
try {
await fse.access(cachePath, fse.constants.R_OK)
} catch (err) {
return false
}
res.type(path.extname(assetPath))
res.type(extName)
await new Promise(resolve => res.sendFile(cachePath, { dotfiles: 'deny' }, resolve))
return true
}
@ -219,11 +219,14 @@ export class Asset extends Model {
return false
}
static async getAssetFromDb(assetPath, fileHash, cachePath, res) {
const asset = await WIKI.db.assets.query().where('hash', fileHash).first()
static async getAssetFromDb({ pathArgs, fileHash, cachePath, siteId }, res) {
const asset = await WIKI.db.knex('tree').where({
siteId,
hash: fileHash
}).first()
if (asset) {
const assetData = await WIKI.db.knex('assetData').where('id', asset.id).first()
res.type(asset.ext)
const assetData = await WIKI.db.knex('assets').where('id', asset.id).first()
res.type(assetData.fileExt)
res.send(assetData.data)
await fse.outputFile(cachePath, assetData.data)
} else {

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#FFC107" d="M37,5H11l-5,7v28c0,1.657,1.343,3,3,3h30c1.656,0,3-1.343,3-3v-5V12L37,5z"/><path fill="#0097A7" d="M33,30c0,4.971-4.029,9-9,9c-4.971,0-9-4.029-9-9s4.029-9,9-9C28.971,21,33,25.029,33,30"/><path fill="#EEE" d="M30.631,30c0,3.664-2.969,6.632-6.631,6.632c-3.663,0-6.632-2.968-6.632-6.632c0-3.663,2.969-6.631,6.632-6.631C27.662,23.369,30.631,26.337,30.631,30"/><path d="M25 29.563L25 25 23 25 23 29.609 23 30.438 23.61 31 25.996 33.119 27.352 31.648z"/><path fill="#DB8509" d="M12.029,7l-3.571,5H18c0,3.314,2.687,6,6,6c3.313,0,6-2.686,6-6h9.542l-3.571-5H12.029z"/></svg>

After

Width:  |  Height:  |  Size: 676 B

@ -15,8 +15,31 @@
icon='mdi-image-plus-outline'
padding='sm sm'
flat
@click='insertAssets'
)
q-menu(anchor='top right' self='top left')
q-list(separator, auto-close)
q-item(
clickable
@click='insertAssets'
)
q-item-section(side)
q-icon(name='las la-folder-open', color='positive')
q-item-section
q-item-label From File Manager...
q-item(
clickable
)
q-item-section(side)
q-icon(name='las la-clipboard', color='brown')
q-item-section
q-item-label From Clipboard...
q-item(
clickable
)
q-item-section(side)
q-icon(name='las la-cloud-download-alt', color='blue')
q-item-section
q-item-label From Remote URL...
q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertAssets') }}
q-btn(
icon='mdi-code-json'
@ -232,6 +255,7 @@ import { get, flatten, last, times, startsWith, debounce } from 'lodash-es'
import { DateTime } from 'luxon'
import * as monaco from 'monaco-editor'
import { Position, Range } from 'monaco-editor'
import { v4 as uuid } from 'uuid'
import { useEditorStore } from 'src/stores/editor'
import { usePageStore } from 'src/stores/page'
@ -596,6 +620,28 @@ onMounted(async () => {
}
}, 500))
// -> Handle asset drop
editor.getContainerDomNode().addEventListener('drop', ev => {
ev.preventDefault()
for (const file of ev.dataTransfer.files) {
const blobUrl = URL.createObjectURL(file)
editorStore.pendingAssets.push({
id: uuid(),
file,
blobUrl
})
if (file.type.startsWith('image')) {
insertAtCursor({
content: `![${file.name}](${blobUrl})`
})
} else {
insertAtCursor({
content: `[${file.name}](${blobUrl})`
})
}
}
})
// -> Post init
editor.focus()

@ -19,6 +19,50 @@
disable
)
q-tooltip(anchor='center left' self='center right') Page Data
q-btn.q-py-md(
v-if='editorStore.isActive'
flat
color='white'
:text-color='hasPendingAssets ? `white` : `deep-orange-3`'
aria-label='Pending Asset Uploads'
)
q-icon(name='mdi-image-sync-outline')
q-badge.page-actions-pending-badge(
v-if='hasPendingAssets'
color='white'
text-color='orange-9'
rounded
floating
)
strong {{ editorStore.pendingAssets.length * 1 }}
q-tooltip(anchor='center left' self='center right') Pending Asset Uploads
q-menu(
ref='menuPendingAssets'
anchor='top left'
self='top right'
:offset='[10, 0]'
)
q-card(style='width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/color-data-pending.svg', left, size='sm')
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-section(side)
q-icon(name='las la-file-image')
q-item-section {{ item.file.name }}
q-item-section(side)
q-btn.acrylic-btn(
color='negative'
round
icon='las la-times'
size='xs'
flat
@click='removePendingAsset(item)'
)
q-card-section.card-actions
em.text-caption Assets that are pasted or dropped onto this page will be held here until the page is saved.
q-separator.q-my-sm(inset)
q-btn.q-py-md(
flat
@ -131,6 +175,14 @@ const route = useRoute()
const { t } = useI18n()
// REFS
const menuPendingAssets = ref(null)
// COMPUTED
const hasPendingAssets = computed(() => editorStore.pendingAssets?.length > 0)
// METHODS
function togglePageProperties () {
@ -188,6 +240,14 @@ function deletePage () {
router.replace('/')
})
}
function removePendingAsset (item) {
URL.revokeObjectURL(item.blobUrl)
editorStore.pendingAssets = editorStore.pendingAssets.filter(a => a.id !== item.id)
if (editorStore.pendingAssets.length < 1) {
menuPendingAssets.value.hide()
}
}
</script>
<style lang="scss">
@ -217,5 +277,21 @@ function deletePage () {
color: $deep-orange-3;
font-weight: 500;
}
&-pending-badge {
animation: pageActionsBadgePulsate 2s ease infinite;
}
}
@keyframes pageActionsBadgePulsate {
0% {
transform: translate(0, 0);
}
50% {
transform: translate(3px, -3px);
}
100% {
transform: translate(0, 0);
}
}
</style>

@ -63,6 +63,7 @@ q-card.page-properties-dialog
color='primary'
)
q-input(
v-if='pageStore.path !== `home`'
v-model='pageStore.alias'
:label='t(`editor.props.alias`)'
outlined

@ -24,7 +24,8 @@ export const useEditorStore = defineStore('editor', {
editors: {},
configIsLoaded: false,
reasonForChange: '',
ignoreRouteChange: false
ignoreRouteChange: false,
pendingAssets: []
}),
getters: {
hasPendingChanges: (state) => {

Loading…
Cancel
Save