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
a6041b4b
Unverified
Commit
a6041b4b
authored
Apr 07, 2023
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: editor reason for change + nav fixes
parent
9a92789d
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
271 additions
and
110 deletions
+271
-110
user.js
server/graph/resolvers/user.js
+4
-3
fluent-query.svg
ux/public/_assets/icons/fluent-query.svg
+2
-0
EditorMarkdown.vue
ux/src/components/EditorMarkdown.vue
+4
-0
PageHeader.vue
ux/src/components/PageHeader.vue
+30
-7
PageReasonForChangeDialog.vue
ux/src/components/PageReasonForChangeDialog.vue
+105
-0
en.json
ux/src/i18n/locales/en.json
+5
-0
Index.vue
ux/src/pages/Index.vue
+5
-0
editor.js
ux/src/stores/editor.js
+4
-3
page.js
ux/src/stores/page.js
+112
-97
No files found.
server/graph/resolvers/user.js
View file @
a6041b4b
...
...
@@ -283,11 +283,12 @@ module.exports = {
async
uploadUserAvatar
(
obj
,
args
)
{
try
{
const
{
filename
,
mimetype
,
createReadStream
}
=
await
args
.
image
WIKI
.
logger
.
debug
(
`Processing user
${
args
.
id
}
avatar
${
filename
}
of type
${
mimetype
}
...`
)
const
lowercaseFilename
=
filename
.
toLowerCase
()
WIKI
.
logger
.
debug
(
`Processing user
${
args
.
id
}
avatar
${
lowercaseFilename
}
of type
${
mimetype
}
...`
)
if
(
!
WIKI
.
extensions
.
ext
.
sharp
.
isInstalled
)
{
throw
new
Error
(
'This feature requires the Sharp extension but it is not installed.'
)
throw
new
Error
(
'This feature requires the Sharp extension but it is not installed.
Contact your wiki administrator.
'
)
}
if
(
!
[
'.png'
,
'.jpg'
,
'.webp'
,
'.gif'
].
some
(
s
=>
f
ilename
.
endsWith
(
s
)))
{
if
(
!
[
'.png'
,
'.jpg'
,
'.webp'
,
'.gif'
].
some
(
s
=>
lowercaseF
ilename
.
endsWith
(
s
)))
{
throw
new
Error
(
'Invalid File Extension. Must be png, jpg, webp or gif.'
)
}
const
destFolder
=
path
.
resolve
(
...
...
ux/public/_assets/icons/fluent-query.svg
0 → 100644
View file @
a6041b4b
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 48 48"
width=
"96px"
height=
"96px"
><linearGradient
id=
"ynnI1ERbfrjO3mVU4UpGia"
x1=
"15.783"
x2=
"38.127"
y1=
"45.91"
y2=
"3.888"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#bd4ff4"
/><stop
offset=
".587"
stop-color=
"#a235ec"
/><stop
offset=
"1"
stop-color=
"#8c20e5"
/></linearGradient><path
fill=
"url(#ynnI1ERbfrjO3mVU4UpGia)"
d=
"M42,10c-11.122,0-11.278-6-18-6s-6.878,6-18,6c-0.552,0-1,0.448-1,1c0,0,0,5.856,0,9 c0,1.378,0.178,2.712,0.493,4c2.936,12.007,18.08,19.907,18.08,19.907S23.784,44,24,44c0.203,0,0.427-0.093,0.427-0.093 s15.144-7.9,18.08-19.907C42.822,22.712,43,21.378,43,20c0-3.144,0-9,0-9C43,10.448,42.552,10,42,10z"
/><path
d=
"M22.155,29c-0.691,0-1.29-0.469-1.457-1.14 c-0.077-0.308-0.208-0.926-0.208-1.586c0-2.688,1.457-4.01,2.627-5.073c0.909-0.827,1.51-1.372,1.51-2.362 c0-0.885-0.645-1.071-1.187-1.071c-1.284,0-2.382,0.582-3.078,1.07c-0.256,0.18-0.556,0.275-0.866,0.275 c-0.388,0-0.756-0.147-1.036-0.416C18.164,18.414,18,18.029,18,17.614v-3.007c0-0.536,0.291-1.036,0.759-1.305 C20.265,12.438,22.003,12,23.926,12C30.49,12,31,16.75,31,18.206c0,3.202-1.894,4.871-3.276,6.089 c-0.82,0.723-1.529,1.348-1.605,2.039c-0.025,0.226,0,0.526,0.068,0.824c0.104,0.452-0.001,0.918-0.289,1.278 C25.613,28.794,25.186,29,24.728,29H22.155z"
opacity=
".05"
/><path
d=
"M22.155,28.5c-0.461,0-0.86-0.313-0.971-0.76 c-0.072-0.286-0.193-0.86-0.193-1.466c0-2.465,1.309-3.654,2.463-4.703c0.935-0.85,1.674-1.521,1.674-2.732 c0-1.458-1.291-1.571-1.687-1.571c-1.415,0-2.611,0.631-3.365,1.161c-0.172,0.121-0.372,0.185-0.579,0.185 c-0.258,0-0.504-0.098-0.691-0.277c-0.198-0.189-0.307-0.445-0.307-0.723v-3.007c0-0.357,0.195-0.691,0.508-0.871 c1.428-0.82,3.083-1.236,4.917-1.236c5.934,0,6.574,3.99,6.574,5.706c0,2.976-1.723,4.495-3.106,5.714 c-0.898,0.792-1.674,1.476-1.771,2.359c-0.031,0.284-0.003,0.636,0.078,0.99c0.069,0.303,0,0.614-0.192,0.855 c-0.19,0.238-0.474,0.375-0.78,0.375h-2.572V28.5z"
opacity=
".07"
/><path
fill=
"#fff"
d=
"M22.155,28c-0.231,0-0.43-0.155-0.486-0.38c-0.087-0.349-0.178-0.849-0.178-1.346 c0-4.177,4.137-4.337,4.137-7.435c0-1.95-1.854-2.071-2.187-2.071c-1.519,0-2.798,0.651-3.652,1.251 C19.455,18.254,19,18.022,19,17.614v-3.007c0-0.182,0.099-0.347,0.257-0.438C20.067,13.705,21.61,13,23.926,13 C29.924,13,30,17.286,30,18.206c0,4.695-4.588,5.421-4.875,8.019c-0.044,0.4,0.011,0.822,0.087,1.157 C25.285,27.699,25.053,28,24.728,28H22.155z"
/><path
d=
"M23.818,35c-1.56,0-3.218-1.056-3.218-3.012 c0-1.94,1.658-2.988,3.218-2.988C25.399,29,27,30.026,27,31.988C27,33.965,25.399,35,23.818,35z"
opacity=
".05"
/><path
d=
"M23.818,34.5c-1.317,0-2.718-0.88-2.718-2.512 c0-1.616,1.4-2.488,2.718-2.488c1.292,0,2.682,0.778,2.682,2.488C26.5,33.714,25.11,34.5,23.818,34.5z"
opacity=
".07"
/><path
fill=
"#fff"
d=
"M23.818,34c-1.078,0-2.218-0.683-2.218-2.012S22.784,30,23.818,30S26,30.589,26,31.988 S24.896,34,23.818,34z"
/></svg>
\ No newline at end of file
ux/src/components/EditorMarkdown.vue
View file @
a6041b4b
...
...
@@ -237,6 +237,7 @@ import { reactive, ref, shallowRef, nextTick, onBeforeMount, onMounted, watch }
import
{
useMeta
,
useQuasar
,
setCssVar
}
from
'quasar'
import
{
useI18n
}
from
'vue-i18n'
import
{
get
,
flatten
,
last
,
times
,
startsWith
,
debounce
}
from
'lodash-es'
import
{
DateTime
}
from
'luxon'
import
{
useEditorStore
}
from
'src/stores/editor'
import
{
usePageStore
}
from
'src/stores/page'
...
...
@@ -438,6 +439,9 @@ onMounted(async () => {
cm
.
value
.
setValue
(
pageStore
.
content
)
cm
.
value
.
on
(
'change'
,
c
=>
{
editorStore
.
$patch
({
lastChangeTimestamp
:
DateTime
.
utc
()
}
)
pageStore
.
$patch
({
content
:
c
.
getValue
()
}
)
...
...
ux/src/components/PageHeader.vue
View file @
a6041b4b
...
...
@@ -120,8 +120,8 @@
flat
icon='las la-times'
color='negative'
label='Discard
'
aria-label='Discard
'
:label='editorStore.hasPendingChanges ? t(`common.actions.discard`) : t(`common.actions.close`)
'
:aria-label='editorStore.hasPendingChanges ? t(`common.actions.discard`) : t(`common.actions.close`)
'
no-caps
@click='discardChanges'
)
...
...
@@ -142,6 +142,7 @@
color='positive'
label='Save Changes'
aria-label='Save Changes'
:disabled='!editorStore.hasPendingChanges'
no-caps
@click='saveChanges'
)
...
...
@@ -220,17 +221,21 @@ async function discardChanges () {
return
}
const
hadPendingChanges
=
editorStore
.
hasPendingChanges
$q
.
loading
.
show
()
try
{
editorStore
.
$patch
({
isActive
:
false
,
editor
:
''
})
await
pageStore
.
pageLoad
({
id
:
pageStore
.
id
})
$q
.
notify
({
type
:
'positive'
,
message
:
'Page has been reverted to the last saved state.'
})
await
pageStore
.
cancelPageEdit
()
if
(
hadPendingChanges
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
'Page has been reverted to the last saved state.'
})
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
...
...
@@ -241,6 +246,24 @@ async function discardChanges () {
}
async
function
saveChanges
()
{
if
(
siteStore
.
features
.
reasonForChange
!==
'off'
)
{
$q
.
dialog
({
component
:
defineAsyncComponent
(()
=>
import
(
'../components/PageReasonForChangeDialog.vue'
)),
componentProps
:
{
required
:
siteStore
.
features
.
reasonForChange
===
'required'
}
}).
onOk
(
async
({
reason
})
=>
{
editorStore
.
$patch
({
reasonForChange
:
reason
})
saveChangesCommit
()
})
}
else
{
saveChangesCommit
()
}
}
async
function
saveChangesCommit
()
{
$q
.
loading
.
show
()
try
{
await
pageStore
.
pageSave
()
...
...
ux/src/components/PageReasonForChangeDialog.vue
0 → 100644
View file @
a6041b4b
<
template
lang=
"pug"
>
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-query.svg', left, size='sm')
span
{{
t
(
`editor.reasonForChange.title`
)
}}
q-card-section
.text-body2(v-if='props.required')
{{
t
(
`editor.reasonForChange.required`
)
}}
.text-body2(v-else)
{{
t
(
`editor.reasonForChange.optional`
)
}}
q-form.q-pb-sm(ref='reasonForm', @submit.prevent='commit')
q-item
q-item-section
q-input(
outlined
v-model='state.reason'
dense
:rules='reasonValidation'
hide-bottom-space
:label='t(`editor.reasonForChange.field`)'
:aria-label='t(`editor.reasonForChange.field`)'
lazy-rules='ondemand'
autofocus
)
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.save`)'
color='primary'
padding='xs md'
@click='commit'
:loading='state.isLoading'
)
</
template
>
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
reactive
,
ref
}
from
'vue'
// PROPS
const
props
=
defineProps
({
required
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
}
})
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
reason
:
''
,
isLoading
:
false
})
// REFS
const
reasonForm
=
ref
(
null
)
// VALIDATION RULES
const
reasonValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'editor.reasonForChange.reasonMissing'
)
]
// METHODS
async
function
commit
()
{
state
.
isLoading
=
true
try
{
if
(
props
.
required
)
{
const
isFormValid
=
await
reasonForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
'Form Invalid'
)
}
}
onDialogOK
({
reason
:
state
.
reason
})
}
catch
(
err
)
{
}
state
.
isLoading
=
false
}
</
script
>
ux/src/i18n/locales/en.json
View file @
a6041b4b
...
...
@@ -1531,6 +1531,11 @@
"editor.props.title"
:
"Title"
,
"editor.props.tocMinMaxDepth"
:
"Min/Max Depth"
,
"editor.props.visibility"
:
"Visibility"
,
"editor.reasonForChange.field"
:
"Reason"
,
"editor.reasonForChange.optional"
:
"Enter a short description of the reason for this change. This is optional but recommended."
,
"editor.reasonForChange.reasonMissing"
:
"A reason is missing."
,
"editor.reasonForChange.required"
:
"You must provide a reason for this change. Enter a small description of what changed."
,
"editor.reasonForChange.title"
:
"Reason For Change"
,
"editor.renderPreview"
:
"Render Preview"
,
"editor.save.createSuccess"
:
"Page created successfully."
,
"editor.save.error"
:
"An error occurred while creating the page"
,
...
...
ux/src/pages/Index.vue
View file @
a6041b4b
...
...
@@ -261,6 +261,11 @@ watch(() => route.path, async (newValue) => {
if
(
newValue
.
startsWith
(
'/_'
))
{
return
}
try
{
await
pageStore
.
pageLoad
({
path
:
newValue
})
if
(
editorStore
.
isActive
)
{
editorStore
.
$patch
({
isActive
:
false
})
}
}
catch
(
err
)
{
if
(
err
.
message
===
'ERR_PAGE_NOT_FOUND'
)
{
if
(
newValue
===
'/'
)
{
...
...
ux/src/stores/editor.js
View file @
a6041b4b
...
...
@@ -4,8 +4,8 @@ export const useEditorStore = defineStore('editor', {
state
:
()
=>
({
isActive
:
false
,
editor
:
''
,
content
:
''
,
mode
:
'
create
'
,
originPageId
:
''
,
mode
:
'
edit
'
,
activeModal
:
''
,
activeModalData
:
null
,
hideSideNav
:
false
,
...
...
@@ -17,7 +17,8 @@ export const useEditorStore = defineStore('editor', {
checkoutDateActive
:
''
,
lastSaveTimestamp
:
null
,
lastChangeTimestamp
:
null
,
editors
:
{}
editors
:
{},
reasonForChange
:
''
}),
getters
:
{
hasPendingChanges
:
(
state
)
=>
{
...
...
ux/src/stores/page.js
View file @
a6041b4b
...
...
@@ -109,6 +109,73 @@ const gqlQueries = {
`
}
const
gqlMutations
=
{
createPage
:
gql
`
mutation createPage (
$allowComments: Boolean
$allowContributions: Boolean
$allowRatings: Boolean
$content: String!
$description: String!
$editor: String!
$icon: String
$isBrowsable: Boolean
$locale: String!
$path: String!
$publishState: PagePublishState!
$publishEndDate: Date
$publishStartDate: Date
$relations: [PageRelationInput!]
$scriptCss: String
$scriptJsLoad: String
$scriptJsUnload: String
$showSidebar: Boolean
$showTags: Boolean
$showToc: Boolean
$siteId: UUID!
$tags: [String!]
$title: String!
$tocDepth: PageTocDepthInput
) {
createPage (
allowComments: $allowComments
allowContributions: $allowContributions
allowRatings: $allowRatings
content: $content
description: $description
editor: $editor
icon: $icon
isBrowsable: $isBrowsable
locale: $locale
path: $path
publishState: $publishState
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
relations: $relations
scriptCss: $scriptCss
scriptJsLoad: $scriptJsLoad
scriptJsUnload: $scriptJsUnload
showSidebar: $showSidebar
showTags: $showTags
showToc: $showToc
siteId: $siteId
tags: $tags
title: $title
tocDepth: $tocDepth
) {
operation {
succeeded
message
}
page {
...PageRead
}
}
}
${
pagePropsFragment
}
`
}
export
const
usePageStore
=
defineStore
(
'page'
,
{
state
:
()
=>
({
allowComments
:
false
,
...
...
@@ -211,45 +278,37 @@ export const usePageStore = defineStore('page', {
pageCreate
({
editor
,
locale
,
path
,
title
=
''
,
description
=
''
,
content
=
''
})
{
const
editorStore
=
useEditorStore
()
// if (['markdown', 'api'].includes(editor)) {
// commit('site/SET_SHOW_SIDE_NAV', false, { root: true })
// } else {
// commit('site/SET_SHOW_SIDE_NAV', true, { root: true })
// }
// if (['markdown', 'channel', 'api'].includes(editor)) {
// commit('site/SET_SHOW_SIDEBAR', false, { root: true })
// } else {
// commit('site/SET_SHOW_SIDEBAR', true, { root: true })
// }
// -> Init editor
editorStore
.
$patch
({
originPageId
:
editorStore
.
isActive
?
editorStore
.
originPageId
:
this
.
id
,
// Don't replace if already in edit mode
isActive
:
true
,
mode
:
'create'
,
editor
})
// -> Page Data
this
.
id
=
0
this
.
locale
=
locale
||
this
.
locale
if
(
path
||
path
===
''
)
{
this
.
path
=
path
}
else
{
this
.
path
=
this
.
path
.
length
<
2
?
'new-page'
:
`
${
this
.
path
}
/new-page`
// -> Default Page Path
let
newPath
=
path
if
(
!
path
&&
path
!==
''
)
{
newPath
=
this
.
path
.
length
<
2
?
'new-page'
:
`
${
this
.
path
}
/new-page`
}
this
.
title
=
title
??
''
this
.
description
=
description
??
''
this
.
icon
=
'las la-file-alt'
this
.
publishState
=
'published'
this
.
relations
=
[]
this
.
tags
=
[]
this
.
content
=
content
??
''
this
.
render
=
''
// -> View Mode
this
.
mode
=
'edit'
// -> Editor Mode
editorStore
.
$patch
({
isActive
:
true
,
editor
,
mode
:
'create'
// -> Set Default Page Data
this
.
$patch
({
id
:
0
,
locale
:
locale
||
this
.
locale
,
path
:
newPath
,
title
:
title
??
''
,
description
:
description
??
''
,
icon
:
'las la-file-alt'
,
publishState
:
'published'
,
relations
:
[],
tags
:
[],
content
:
content
??
''
,
render
:
''
,
mode
:
'edit'
})
this
.
router
.
push
(
'/_create'
)
},
/**
* PAGE SAVE
...
...
@@ -260,66 +319,7 @@ export const usePageStore = defineStore('page', {
try
{
if
(
editorStore
.
mode
===
'create'
)
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation createPage (
$allowComments: Boolean
$allowContributions: Boolean
$allowRatings: Boolean
$content: String!
$description: String!
$editor: String!
$icon: String
$isBrowsable: Boolean
$locale: String!
$path: String!
$publishState: PagePublishState!
$publishEndDate: Date
$publishStartDate: Date
$relations: [PageRelationInput!]
$scriptCss: String
$scriptJsLoad: String
$scriptJsUnload: String
$showSidebar: Boolean
$showTags: Boolean
$showToc: Boolean
$siteId: UUID!
$tags: [String!]
$title: String!
$tocDepth: PageTocDepthInput
) {
createPage (
allowComments: $allowComments
allowContributions: $allowContributions
allowRatings: $allowRatings
content: $content
description: $description
editor: $editor
icon: $icon
isBrowsable: $isBrowsable
locale: $locale
path: $path
publishState: $publishState
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
relations: $relations
scriptCss: $scriptCss
scriptJsLoad: $scriptJsLoad
scriptJsUnload: $scriptJsUnload
showSidebar: $showSidebar
showTags: $showTags
showToc: $showToc
siteId: $siteId
tags: $tags
title: $title
tocDepth: $tocDepth
) {
operation {
succeeded
message
}
}
}
`
,
mutation
:
gqlMutations
.
createPage
,
variables
:
{
...
pick
(
this
,
[
'allowComments'
,
...
...
@@ -354,8 +354,18 @@ export const usePageStore = defineStore('page', {
if
(
!
result
.
succeeded
)
{
throw
new
Error
(
result
.
message
)
}
this
.
id
=
resp
.
data
.
createPage
.
page
.
id
this
.
editor
=
editorStore
.
editor
const
pageData
=
cloneDeep
(
resp
.
data
.
createPage
.
page
??
{})
if
(
!
pageData
?.
id
)
{
throw
new
Error
(
'ERR_CREATED_PAGE_NOT_FOUND'
)
}
// Update page store
this
.
$patch
({
...
pageData
,
relations
:
pageData
.
relations
.
map
(
r
=>
pick
(
r
,
[
'id'
,
'position'
,
'label'
,
'caption'
,
'icon'
,
'target'
])),
tocDepth
:
pick
(
pageData
.
tocDepth
,
[
'min'
,
'max'
])
})
this
.
router
.
replace
(
`/
${
this
.
path
}
`
)
}
else
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
...
...
@@ -419,6 +429,11 @@ export const usePageStore = defineStore('page', {
throw
err
}
},
async
cancelPageEdit
()
{
const
editorStore
=
useEditorStore
()
await
this
.
pageLoad
({
id
:
editorStore
.
originPageId
?
editorStore
.
originPageId
:
this
.
id
})
this
.
router
.
replace
(
`/
${
this
.
path
}
`
)
},
generateToc
()
{
}
...
...
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