feat: editor reason for change + nav fixes

pull/6775/head
NGPixel 2 years ago
parent 9a92789d13
commit a6041b4ba5
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -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 => filename.endsWith(s))) {
if (!['.png', '.jpg', '.webp', '.gif'].some(s => lowercaseFilename.endsWith(s))) {
throw new Error('Invalid File Extension. Must be png, jpg, webp or gif.')
}
const destFolder = path.resolve(

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -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()
})

@ -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 })
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()

@ -0,0 +1,105 @@
<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>

@ -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",

@ -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 === '/') {

@ -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) => {

@ -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 })
// }
// -> 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`
}
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
// -> Init editor
editorStore.$patch({
originPageId: editorStore.isActive ? editorStore.originPageId : this.id, // Don't replace if already in edit mode
isActive: true,
editor,
mode: 'create'
mode: 'create',
editor
})
// -> Default Page Path
let newPath = path
if (!path && path !== '') {
newPath = this.path.length < 2 ? 'new-page' : `${this.path}/new-page`
}
// -> 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 () {
}

Loading…
Cancel
Save