Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
5a8d95ee
Unverified
Commit
5a8d95ee
authored
Jul 31, 2023
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: editor pending asset uploads (wip)
parent
607b8d81
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
166 additions
and
23 deletions
+166
-23
README.md
README.md
+1
-1
common.mjs
server/controllers/common.mjs
+17
-3
assets.mjs
server/models/assets.mjs
+20
-17
color-data-pending.svg
ux/public/_assets/icons/color-data-pending.svg
+2
-0
EditorMarkdown.vue
ux/src/components/EditorMarkdown.vue
+47
-1
PageActionsCol.vue
ux/src/components/PageActionsCol.vue
+76
-0
PagePropertiesDialog.vue
ux/src/components/PagePropertiesDialog.vue
+1
-0
editor.js
ux/src/stores/editor.js
+2
-1
No files found.
README.md
View file @
5a8d95ee
...
@@ -66,7 +66,7 @@ The current stable release (2.x) is available at https://js.wiki
...
@@ -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
:
1. In the left-side terminal
(
Server
)
, run the
command
:
```
sh
```
sh
n
ode
run start
n
pm
run start
```
```
1. Open your browser to
`
http://localhost:3000
`
1. Open your browser to
`
http://localhost:3000
`
1. Login using the default administrator user:
1. Login using the default administrator user:
...
...
server/controllers/common.mjs
View file @
5a8d95ee
import
express
from
'express'
import
express
from
'express'
// import pageHelper
from '../helpers/page.mjs'
import
{
parsePath
}
from
'../helpers/page.mjs'
// import CleanCSS from 'clean-css'
// import CleanCSS from 'clean-css'
import
path
from
'node:path'
import
path
from
'node:path'
...
@@ -526,8 +526,22 @@ export default function () {
...
@@ -526,8 +526,22 @@ export default function () {
// }
// }
// })
// })
router
.
get
(
'/*'
,
(
req
,
res
,
next
)
=>
{
router
.
get
(
'/*'
,
async
(
req
,
res
,
next
)
=>
{
res
.
sendFile
(
path
.
join
(
WIKI
.
ROOTPATH
,
'assets/index.html'
))
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
return
router
...
...
server/models/assets.mjs
View file @
5a8d95ee
import
{
Model
}
from
'objection'
import
{
Model
}
from
'objection'
import
path
from
'path'
import
path
from
'
node:
path'
import
fse
from
'fs-extra'
import
fse
from
'fs-extra'
import
{
startsWith
}
from
'lodash-es'
import
{
startsWith
}
from
'lodash-es'
import
{
generateHash
}
from
'../helpers/common.mjs'
import
{
generateHash
}
from
'../helpers/common.mjs'
...
@@ -166,24 +166,24 @@ export class Asset extends Model {
...
@@ -166,24 +166,24 @@ export class Asset extends Model {
.
first
()
.
first
()
}
}
static
async
getAsset
({
path
,
locale
,
siteId
},
res
)
{
static
async
getAsset
({
path
Args
,
siteId
},
res
)
{
try
{
try
{
const
fileInfo
=
''
// assetHelper.getPathInfo(assetPath
)
const
fileInfo
=
path
.
parse
(
pathArgs
.
path
.
toLowerCase
()
)
const
fileHash
=
''
// assetHelper.generateHash(assetP
ath)
const
fileHash
=
generateHash
(
pathArgs
.
p
ath
)
const
cachePath
=
path
.
resolve
(
WIKI
.
ROOTPATH
,
WIKI
.
config
.
dataPath
,
`cache/
${
fileHash
}
.dat`
)
const
cachePath
=
path
.
resolve
(
WIKI
.
ROOTPATH
,
WIKI
.
config
.
dataPath
,
`cache/
${
siteId
}
/
${
fileHash
}
.dat`
)
// Force unsafe extensions to download
// Force unsafe extensions to download
if
(
WIKI
.
config
.
uploads
.
force
Download
&&
!
[
'.png'
,
'.apng'
,
'.jpg'
,
'.jpeg'
,
'.gif'
,
'.bmp'
,
'.webp'
,
'.svg'
].
includes
(
fileInfo
.
ext
))
{
if
(
WIKI
.
config
.
security
.
forceAsset
Download
&&
!
[
'.png'
,
'.apng'
,
'.jpg'
,
'.jpeg'
,
'.gif'
,
'.bmp'
,
'.webp'
,
'.svg'
].
includes
(
fileInfo
.
ext
))
{
res
.
set
(
'Content-disposition'
,
'attachment; filename='
+
encodeURIComponent
(
fileInfo
.
base
))
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
return
}
}
if
(
await
WIKI
.
db
.
assets
.
getAssetFromStorage
(
assetPath
,
res
))
{
//
if (await WIKI.db.assets.getAssetFromStorage(assetPath, res)) {
return
//
return
}
//
}
await
WIKI
.
db
.
assets
.
getAssetFromDb
(
assetPath
,
fileHash
,
cachePath
,
res
)
await
WIKI
.
db
.
assets
.
getAssetFromDb
(
{
pathArgs
,
fileHash
,
cachePath
,
siteId
}
,
res
)
}
catch
(
err
)
{
}
catch
(
err
)
{
if
(
err
.
code
===
`ECONNABORTED`
||
err
.
code
===
`EPIPE`
)
{
if
(
err
.
code
===
`ECONNABORTED`
||
err
.
code
===
`EPIPE`
)
{
return
return
...
@@ -193,13 +193,13 @@ export class Asset extends Model {
...
@@ -193,13 +193,13 @@ export class Asset extends Model {
}
}
}
}
static
async
getAssetFromCache
(
assetPath
,
cachePath
,
res
)
{
static
async
getAssetFromCache
(
{
cachePath
,
extName
}
,
res
)
{
try
{
try
{
await
fse
.
access
(
cachePath
,
fse
.
constants
.
R_OK
)
await
fse
.
access
(
cachePath
,
fse
.
constants
.
R_OK
)
}
catch
(
err
)
{
}
catch
(
err
)
{
return
false
return
false
}
}
res
.
type
(
path
.
extname
(
assetPath
)
)
res
.
type
(
extName
)
await
new
Promise
(
resolve
=>
res
.
sendFile
(
cachePath
,
{
dotfiles
:
'deny'
},
resolve
))
await
new
Promise
(
resolve
=>
res
.
sendFile
(
cachePath
,
{
dotfiles
:
'deny'
},
resolve
))
return
true
return
true
}
}
...
@@ -219,11 +219,14 @@ export class Asset extends Model {
...
@@ -219,11 +219,14 @@ export class Asset extends Model {
return
false
return
false
}
}
static
async
getAssetFromDb
(
assetPath
,
fileHash
,
cachePath
,
res
)
{
static
async
getAssetFromDb
({
pathArgs
,
fileHash
,
cachePath
,
siteId
},
res
)
{
const
asset
=
await
WIKI
.
db
.
assets
.
query
().
where
(
'hash'
,
fileHash
).
first
()
const
asset
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
({
siteId
,
hash
:
fileHash
}).
first
()
if
(
asset
)
{
if
(
asset
)
{
const
assetData
=
await
WIKI
.
db
.
knex
(
'asset
Data
'
).
where
(
'id'
,
asset
.
id
).
first
()
const
assetData
=
await
WIKI
.
db
.
knex
(
'asset
s
'
).
where
(
'id'
,
asset
.
id
).
first
()
res
.
type
(
asset
.
e
xt
)
res
.
type
(
asset
Data
.
fileE
xt
)
res
.
send
(
assetData
.
data
)
res
.
send
(
assetData
.
data
)
await
fse
.
outputFile
(
cachePath
,
assetData
.
data
)
await
fse
.
outputFile
(
cachePath
,
assetData
.
data
)
}
else
{
}
else
{
...
...
ux/public/_assets/icons/color-data-pending.svg
0 → 100644
View file @
5a8d95ee
<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>
\ No newline at end of file
ux/src/components/EditorMarkdown.vue
View file @
5a8d95ee
...
@@ -15,8 +15,31 @@
...
@@ -15,8 +15,31 @@
icon='mdi-image-plus-outline'
icon='mdi-image-plus-outline'
padding='sm sm'
padding='sm sm'
flat
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-tooltip(anchor='center right' self='center left')
{{
t
(
'editor.markup.insertAssets'
)
}}
q-btn(
q-btn(
icon='mdi-code-json'
icon='mdi-code-json'
...
@@ -232,6 +255,7 @@ import { get, flatten, last, times, startsWith, debounce } from 'lodash-es'
...
@@ -232,6 +255,7 @@ import { get, flatten, last, times, startsWith, debounce } from 'lodash-es'
import
{
DateTime
}
from
'luxon'
import
{
DateTime
}
from
'luxon'
import
*
as
monaco
from
'monaco-editor'
import
*
as
monaco
from
'monaco-editor'
import
{
Position
,
Range
}
from
'monaco-editor'
import
{
Position
,
Range
}
from
'monaco-editor'
import
{
v4
as
uuid
}
from
'uuid'
import
{
useEditorStore
}
from
'src/stores/editor'
import
{
useEditorStore
}
from
'src/stores/editor'
import
{
usePageStore
}
from
'src/stores/page'
import
{
usePageStore
}
from
'src/stores/page'
...
@@ -596,6 +620,28 @@ onMounted(async () => {
...
@@ -596,6 +620,28 @@ onMounted(async () => {
}
}
}
,
500
))
}
,
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
:
``
}
)
}
else
{
insertAtCursor
({
content
:
`[${file.name
}
](${blobUrl
}
)`
}
)
}
}
}
)
// -> Post init
// -> Post init
editor
.
focus
()
editor
.
focus
()
...
...
ux/src/components/PageActionsCol.vue
View file @
5a8d95ee
...
@@ -19,6 +19,50 @@
...
@@ -19,6 +19,50 @@
disable
disable
)
)
q-tooltip(anchor='center left' self='center right') Page Data
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-separator.q-my-sm(inset)
q-btn.q-py-md(
q-btn.q-py-md(
flat
flat
...
@@ -131,6 +175,14 @@ const route = useRoute()
...
@@ -131,6 +175,14 @@ const route = useRoute()
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
// REFS
const
menuPendingAssets
=
ref
(
null
)
// COMPUTED
const
hasPendingAssets
=
computed
(()
=>
editorStore
.
pendingAssets
?.
length
>
0
)
// METHODS
// METHODS
function
togglePageProperties
()
{
function
togglePageProperties
()
{
...
@@ -188,6 +240,14 @@ function deletePage () {
...
@@ -188,6 +240,14 @@ function deletePage () {
router
.
replace
(
'/'
)
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
>
</
script
>
<
style
lang=
"scss"
>
<
style
lang=
"scss"
>
...
@@ -217,5 +277,21 @@ function deletePage () {
...
@@ -217,5 +277,21 @@ function deletePage () {
color
:
$deep-orange-3
;
color
:
$deep-orange-3
;
font-weight
:
500
;
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
>
</
style
>
ux/src/components/PagePropertiesDialog.vue
View file @
5a8d95ee
...
@@ -63,6 +63,7 @@ q-card.page-properties-dialog
...
@@ -63,6 +63,7 @@ q-card.page-properties-dialog
color='primary'
color='primary'
)
)
q-input(
q-input(
v-if='pageStore.path !== `home`'
v-model='pageStore.alias'
v-model='pageStore.alias'
:label='t(`editor.props.alias`)'
:label='t(`editor.props.alias`)'
outlined
outlined
...
...
ux/src/stores/editor.js
View file @
5a8d95ee
...
@@ -24,7 +24,8 @@ export const useEditorStore = defineStore('editor', {
...
@@ -24,7 +24,8 @@ export const useEditorStore = defineStore('editor', {
editors
:
{},
editors
:
{},
configIsLoaded
:
false
,
configIsLoaded
:
false
,
reasonForChange
:
''
,
reasonForChange
:
''
,
ignoreRouteChange
:
false
ignoreRouteChange
:
false
,
pendingAssets
:
[]
}),
}),
getters
:
{
getters
:
{
hasPendingChanges
:
(
state
)
=>
{
hasPendingChanges
:
(
state
)
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment