mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 13:17:10 -05:00
feat: editor reason for change + nav fixes
This commit is contained in:
@@ -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 })
|
||||
$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()
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
+113
-98
@@ -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 () {
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user