wikijs-fork/client/components/editor.vue

602 lines
20 KiB
Vue
Raw Permalink Normal View History

2018-02-11 05:20:17 +00:00
<template lang="pug">
2020-05-10 22:43:45 +00:00
v-app.editor(:dark='$vuetify.theme.dark')
nav-header(dense)
template(slot='mid')
v-text-field.editor-title-input(
dark
solo
flat
v-model='currentPageTitle'
hide-details
background-color='black'
dense
full-width
)
template(slot='actions')
2020-03-21 23:18:08 +00:00
v-btn.mr-3.animated.fadeIn(color='amber', outlined, small, v-if='isConflict', @click='openConflict')
2020-03-02 05:43:19 +00:00
.overline.amber--text.mr-3 Conflict
status-indicator(intermediary, pulse)
v-btn.animated.fadeInDown(
2019-08-03 04:48:55 +00:00
text
2018-08-13 04:12:44 +00:00
color='green'
2020-03-31 01:36:31 +00:00
@click.exact='save'
@click.ctrl.exact='saveAndClose'
2018-08-13 04:12:44 +00:00
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
)
2019-08-03 04:48:55 +00:00
v-icon(color='green', :left='$vuetify.breakpoint.lgAndUp') mdi-check
2020-03-21 23:18:08 +00:00
span.grey--text(v-if='$vuetify.breakpoint.lgAndUp && mode !== `create` && !isDirty') {{ $t('editor:save.saved') }}
span.white--text(v-else-if='$vuetify.breakpoint.lgAndUp') {{ mode === 'create' ? $t('common:actions.create') : $t('common:actions.save') }}
v-btn.animated.fadeInDown.wait-p1s(
2019-08-03 04:48:55 +00:00
text
2018-08-13 04:12:44 +00:00
color='blue'
@click='openPropsModal'
2019-01-26 23:35:56 +00:00
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": !welcomeMode, "ml-0": welcomeMode }'
2018-08-13 04:12:44 +00:00
)
2019-08-03 04:48:55 +00:00
v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') mdi-tag-text-outline
2019-06-09 01:00:12 +00:00
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('common:actions.page') }}
v-btn.animated.fadeInDown.wait-p2s(
2019-01-07 03:03:34 +00:00
v-if='!welcomeMode'
2019-08-03 04:48:55 +00:00
text
color='red'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
@click='exit'
)
2019-08-03 04:48:55 +00:00
v-icon(color='red', :left='$vuetify.breakpoint.lgAndUp') mdi-close
2019-06-09 01:00:12 +00:00
span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('common:actions.close') }}
v-divider.ml-3(vertical)
v-main
component(:is='currentEditor', :save='save')
2018-12-09 05:46:06 +00:00
editor-modal-properties(v-model='dialogProps')
editor-modal-editorselect(v-model='dialogEditorSelector')
editor-modal-unsaved(v-model='dialogUnsaved', @discard='exitGo')
component(:is='activeModal')
2018-07-22 04:29:39 +00:00
loader(v-model='dialogProgress', :title='$t(`editor:save.processing`)', :subtitle='$t(`editor:save.pleaseWait`)')
notify
2018-02-11 05:20:17 +00:00
</template>
<script>
import _ from 'lodash'
2020-03-02 05:43:19 +00:00
import gql from 'graphql-tag'
2018-07-22 04:29:39 +00:00
import { get, sync } from 'vuex-pathify'
2018-07-15 23:16:19 +00:00
import { AtomSpinner } from 'epic-spinners'
import { Base64 } from 'js-base64'
2020-03-02 05:43:19 +00:00
import { StatusIndicator } from 'vue-status-indicator'
2018-07-15 23:16:19 +00:00
2019-09-08 16:01:17 +00:00
import editorStore from '../store/editor'
2018-07-15 23:16:19 +00:00
/* global WIKI */
WIKI.$store.registerModule('editor', editorStore)
2018-02-11 05:20:17 +00:00
export default {
2019-01-07 03:03:34 +00:00
i18nOptions: { namespaces: 'editor' },
2018-02-11 05:20:17 +00:00
components: {
2018-07-15 23:16:19 +00:00
AtomSpinner,
2020-03-02 05:43:19 +00:00
StatusIndicator,
editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'),
2018-09-16 22:36:15 +00:00
editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
2019-09-08 16:01:17 +00:00
editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.vue'),
editorAsciidoc: () => import(/* webpackChunkName: "editor-asciidoc", webpackMode: "lazy" */ './editor/editor-asciidoc.vue'),
2018-09-16 22:36:15 +00:00
editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
2020-05-10 22:43:45 +00:00
editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),
editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'),
editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue'),
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-media.vue'),
2020-03-21 23:18:08 +00:00
editorModalBlocks: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-blocks.vue'),
2020-07-11 03:11:28 +00:00
editorModalConflict: () => import(/* webpackChunkName: "editor-conflict", webpackMode: "lazy" */ './editor/editor-modal-conflict.vue'),
editorModalDrawio: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-drawio.vue')
2018-09-16 22:36:15 +00:00
},
props: {
locale: {
type: String,
default: 'en'
},
path: {
type: String,
default: 'home'
},
title: {
type: String,
default: 'Untitled Page'
},
description: {
type: String,
default: ''
},
tags: {
type: Array,
default: () => ([])
},
isPublished: {
type: Boolean,
2018-12-09 05:46:06 +00:00
default: true
2018-09-16 22:36:15 +00:00
},
2020-06-20 20:39:36 +00:00
scriptCss: {
type: String,
default: ''
},
publishStartDate: {
type: String,
default: ''
},
publishEndDate: {
type: String,
default: ''
},
2020-06-20 20:39:36 +00:00
scriptJs: {
type: String,
default: ''
},
2018-09-16 22:36:15 +00:00
initEditor: {
type: String,
default: null
},
initMode: {
type: String,
default: 'create'
},
initContent: {
type: String,
default: null
},
pageId: {
type: Number,
default: 0
2020-03-02 05:43:19 +00:00
},
checkoutDate: {
type: String,
default: new Date().toISOString()
},
effectivePermissions: {
type: String,
default: ''
2018-09-16 22:36:15 +00:00
}
},
data() {
return {
2020-03-21 23:18:08 +00:00
isSaving: false,
2020-03-02 05:43:19 +00:00
isConflict: false,
2018-12-09 05:46:06 +00:00
dialogProps: false,
dialogProgress: false,
dialogEditorSelector: false,
dialogUnsaved: false,
exitConfirmed: false,
2020-06-02 01:52:16 +00:00
initContentParsed: '',
savedState: {
description: '',
isPublished: false,
publishEndDate: '',
publishStartDate: '',
tags: '',
2020-06-21 05:04:36 +00:00
title: '',
css: '',
js: ''
2020-06-02 01:52:16 +00:00
}
}
},
2018-07-16 02:40:41 +00:00
computed: {
currentEditor: sync('editor/editor'),
activeModal: sync('editor/activeModal'),
2018-07-22 04:29:39 +00:00
mode: get('editor/mode'),
welcomeMode() { return this.mode === `create` && this.path === `home` },
currentPageTitle: sync('page/title'),
2020-03-21 23:18:08 +00:00
checkoutDateActive: sync('editor/checkoutDateActive'),
currentStyling: get('page/scriptCss'),
isDirty () {
return _.some([
this.initContentParsed !== this.$store.get('editor/content'),
this.locale !== this.$store.get('page/locale'),
this.path !== this.$store.get('page/path'),
2020-06-02 01:52:16 +00:00
this.savedState.title !== this.$store.get('page/title'),
this.savedState.description !== this.$store.get('page/description'),
this.savedState.tags !== this.$store.get('page/tags'),
2020-06-21 05:04:36 +00:00
this.savedState.isPublished !== this.$store.get('page/isPublished'),
this.savedState.publishStartDate !== this.$store.get('page/publishStartDate'),
this.savedState.publishEndDate !== this.$store.get('page/publishEndDate'),
this.savedState.css !== this.$store.get('page/scriptCss'),
this.savedState.js !== this.$store.get('page/scriptJs')
], Boolean)
}
2018-07-16 02:40:41 +00:00
},
watch: {
currentEditor(newValue, oldValue) {
if (newValue !== '' && this.mode === 'create') {
_.delay(() => {
this.dialogProps = true
}, 500)
}
},
currentStyling(newValue) {
this.injectCustomCss(newValue)
}
},
2018-10-29 02:09:58 +00:00
created() {
2020-06-20 20:39:36 +00:00
this.$store.set('page/id', this.pageId)
this.$store.set('page/description', this.description)
this.$store.set('page/isPublished', this.isPublished)
this.$store.set('page/publishStartDate', this.publishStartDate)
this.$store.set('page/publishEndDate', this.publishEndDate)
2020-06-20 20:39:36 +00:00
this.$store.set('page/locale', this.locale)
this.$store.set('page/path', this.path)
this.$store.set('page/tags', this.tags)
this.$store.set('page/title', this.title)
this.$store.set('page/scriptCss', this.scriptCss)
this.$store.set('page/scriptJs', this.scriptJs)
2020-06-20 20:39:36 +00:00
this.$store.set('page/mode', 'edit')
2020-03-21 23:18:08 +00:00
2020-06-02 01:52:16 +00:00
this.setCurrentSavedState()
2020-03-21 23:18:08 +00:00
this.checkoutDateActive = this.checkoutDate
if (this.effectivePermissions) {
this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))
}
2018-10-29 02:09:58 +00:00
},
2018-07-16 02:40:41 +00:00
mounted() {
2018-09-16 22:36:15 +00:00
this.$store.set('editor/mode', this.initMode || 'create')
2019-09-08 16:01:17 +00:00
this.initContentParsed = this.initContent ? Base64.decode(this.initContent) : ''
this.$store.set('editor/content', this.initContentParsed)
if (this.mode === 'create' && !this.initEditor) {
2018-07-16 02:40:41 +00:00
_.delay(() => {
this.dialogEditorSelector = true
2018-07-16 02:40:41 +00:00
}, 500)
2018-09-16 22:36:15 +00:00
} else {
this.currentEditor = `editor${_.startCase(this.initEditor || 'markdown')}`
2018-07-16 02:40:41 +00:00
}
window.onbeforeunload = () => {
if (!this.exitConfirmed && this.initContentParsed !== this.$store.get('editor/content')) {
return this.$t('editor:unsavedWarning')
} else {
return undefined
}
}
2020-03-21 23:18:08 +00:00
this.$root.$on('resetEditorConflict', () => {
this.isConflict = false
})
// this.$store.set('editor/mode', 'edit')
// this.currentEditor = `editorApi`
2018-07-16 02:40:41 +00:00
},
methods: {
2018-12-09 05:46:06 +00:00
openPropsModal(name) {
this.dialogProps = true
},
showProgressDialog(textKey) {
this.dialogProgress = true
},
hideProgressDialog() {
this.dialogProgress = false
},
2020-03-21 23:18:08 +00:00
openConflict() {
this.$root.$emit('saveConflict')
},
async save({ rethrow = false, overwrite = false } = {}) {
this.showProgressDialog('saving')
2020-03-21 23:18:08 +00:00
this.isSaving = true
2020-03-28 18:08:57 +00:00
const saveTimeoutHandle = setTimeout(() => {
throw new Error('Save operation timed out.')
}, 30000)
try {
if (this.$store.get('editor/mode') === 'create') {
// --------------------------------------------
// -> CREATE PAGE
// --------------------------------------------
let resp = await this.$apollo.mutate({
2020-06-20 05:11:05 +00:00
mutation: gql`
mutation (
$content: String!
$description: String!
$editor: String!
$isPrivate: Boolean!
$isPublished: Boolean!
$locale: String!
$path: String!
$publishEndDate: Date
$publishStartDate: Date
$scriptCss: String
$scriptJs: String
$tags: [String]!
$title: String!
) {
pages {
create(
content: $content
description: $description
editor: $editor
isPrivate: $isPrivate
isPublished: $isPublished
locale: $locale
path: $path
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
scriptCss: $scriptCss
scriptJs: $scriptJs
tags: $tags
title: $title
) {
responseResult {
succeeded
errorCode
slug
message
}
page {
id
updatedAt
}
}
}
}
`,
variables: {
content: this.$store.get('editor/content'),
description: this.$store.get('page/description'),
2019-09-08 16:01:17 +00:00
editor: this.$store.get('editor/editorKey'),
locale: this.$store.get('page/locale'),
isPrivate: false,
isPublished: this.$store.get('page/isPublished'),
path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '',
2020-06-20 05:11:05 +00:00
scriptCss: this.$store.get('page/scriptCss'),
scriptJs: this.$store.get('page/scriptJs'),
tags: this.$store.get('page/tags'),
title: this.$store.get('page/title')
}
2018-07-22 04:29:39 +00:00
})
resp = _.get(resp, 'data.pages.create', {})
if (_.get(resp, 'responseResult.succeeded')) {
2020-03-21 23:18:08 +00:00
this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
this.isConflict = false
this.$store.commit('showNotification', {
message: this.$t('editor:save.createSuccess'),
style: 'success',
icon: 'check'
})
this.$store.set('editor/id', _.get(resp, 'page.id'))
this.$store.set('editor/mode', 'update')
2019-08-25 18:23:56 +00:00
this.exitConfirmed = true
window.location.assign(`/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)
} else {
throw new Error(_.get(resp, 'responseResult.message'))
}
2018-07-22 04:29:39 +00:00
} else {
// --------------------------------------------
// -> UPDATE EXISTING PAGE
// --------------------------------------------
2018-07-22 04:29:39 +00:00
2020-03-21 23:18:08 +00:00
const conflictResp = await this.$apollo.query({
query: gql`
query ($id: Int!, $checkoutDate: Date!) {
pages {
checkConflicts(id: $id, checkoutDate: $checkoutDate)
}
}
`,
fetchPolicy: 'network-only',
variables: {
id: this.pageId,
checkoutDate: this.checkoutDateActive
}
})
if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
this.$root.$emit('saveConflict')
throw new Error(this.$t('editor:conflict.warning'))
2020-03-21 23:18:08 +00:00
}
let resp = await this.$apollo.mutate({
2020-06-20 05:11:05 +00:00
mutation: gql`
mutation (
$id: Int!
$content: String
$description: String
$editor: String
$isPrivate: Boolean
$isPublished: Boolean
$locale: String
$path: String
$publishEndDate: Date
$publishStartDate: Date
$scriptCss: String
$scriptJs: String
$tags: [String]
$title: String
) {
pages {
update(
id: $id
content: $content
description: $description
editor: $editor
isPrivate: $isPrivate
isPublished: $isPublished
locale: $locale
path: $path
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
scriptCss: $scriptCss
scriptJs: $scriptJs
tags: $tags
title: $title
) {
responseResult {
succeeded
errorCode
slug
message
}
page {
updatedAt
}
}
}
}
`,
variables: {
id: this.$store.get('page/id'),
content: this.$store.get('editor/content'),
description: this.$store.get('page/description'),
2019-09-08 16:01:17 +00:00
editor: this.$store.get('editor/editorKey'),
locale: this.$store.get('page/locale'),
isPrivate: false,
isPublished: this.$store.get('page/isPublished'),
path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '',
2020-06-20 05:11:05 +00:00
scriptCss: this.$store.get('page/scriptCss'),
scriptJs: this.$store.get('page/scriptJs'),
tags: this.$store.get('page/tags'),
title: this.$store.get('page/title')
}
})
resp = _.get(resp, 'data.pages.update', {})
if (_.get(resp, 'responseResult.succeeded')) {
2020-03-21 23:18:08 +00:00
this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
this.isConflict = false
this.$store.commit('showNotification', {
message: this.$t('editor:save.updateSuccess'),
style: 'success',
icon: 'check'
})
2019-10-13 23:59:50 +00:00
if (this.locale !== this.$store.get('page/locale') || this.path !== this.$store.get('page/path')) {
_.delay(() => {
window.location.replace(`/e/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)
}, 1000)
}
} else {
throw new Error(_.get(resp, 'responseResult.message'))
}
2018-07-22 04:29:39 +00:00
}
2019-01-07 03:03:34 +00:00
this.initContentParsed = this.$store.get('editor/content')
2020-06-02 01:52:16 +00:00
this.setCurrentSavedState()
} catch (err) {
this.$store.commit('showNotification', {
message: err.message,
style: 'error',
icon: 'warning'
})
2020-03-21 23:18:08 +00:00
if (rethrow === true) {
2020-03-28 18:08:57 +00:00
clearTimeout(saveTimeoutHandle)
this.isSaving = false
this.hideProgressDialog()
2020-03-21 23:18:08 +00:00
throw err
}
2018-07-22 04:29:39 +00:00
}
2020-03-28 18:08:57 +00:00
clearTimeout(saveTimeoutHandle)
2020-03-21 23:18:08 +00:00
this.isSaving = false
2018-07-22 04:29:39 +00:00
this.hideProgressDialog()
2018-11-24 02:10:24 +00:00
},
async saveAndClose() {
try {
if (this.$store.get('editor/mode') === 'create') {
await this.save()
} else {
await this.save({ rethrow: true })
await this.exit()
}
} catch (err) {
// Error is already handled
}
},
async exit() {
if (this.isDirty) {
this.dialogUnsaved = true
} else {
this.exitGo()
}
},
exitGo() {
this.$store.commit(`loadingStart`, 'editor-close')
this.currentEditor = ''
this.exitConfirmed = true
_.delay(() => {
2019-01-01 22:08:16 +00:00
if (this.$store.get('editor/mode') === 'create') {
window.location.assign(`/`)
} else {
window.location.assign(`/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)
2019-01-01 22:08:16 +00:00
}
}, 500)
2020-06-02 01:52:16 +00:00
},
setCurrentSavedState () {
this.savedState = {
description: this.$store.get('page/description'),
isPublished: this.$store.get('page/isPublished'),
publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '',
tags: this.$store.get('page/tags'),
2020-06-21 05:04:36 +00:00
title: this.$store.get('page/title'),
css: this.$store.get('page/scriptCss'),
js: this.$store.get('page/scriptJs')
2020-06-02 01:52:16 +00:00
}
},
injectCustomCss: _.debounce(css => {
const oldStyl = document.querySelector('#editor-script-css')
if (oldStyl) {
document.head.removeChild(oldStyl)
}
if (!_.isEmpty(css)) {
const styl = document.createElement('style')
styl.type = 'text/css'
styl.id = 'editor-script-css'
document.head.appendChild(styl)
styl.appendChild(document.createTextNode(css))
}
}, 1000)
2020-03-02 05:43:19 +00:00
},
apollo: {
isConflict: {
query: gql`
query ($id: Int!, $checkoutDate: Date!) {
pages {
checkConflicts(id: $id, checkoutDate: $checkoutDate)
}
}
`,
fetchPolicy: 'network-only',
pollInterval: 5000,
variables () {
return {
id: this.pageId,
2020-03-21 23:18:08 +00:00
checkoutDate: this.checkoutDateActive
2020-03-02 05:43:19 +00:00
}
},
update: (data) => _.cloneDeep(data.pages.checkConflicts),
skip () {
2020-03-21 23:18:08 +00:00
return this.mode === 'create' || this.isSaving || !this.isDirty
2020-03-02 05:43:19 +00:00
}
}
2018-02-11 05:20:17 +00:00
}
}
</script>
<style lang='scss'>
2018-09-16 22:42:41 +00:00
.editor {
2018-12-09 05:46:06 +00:00
background-color: mc('grey', '900') !important;
2018-09-16 22:42:41 +00:00
min-height: 100vh;
2019-01-26 23:35:56 +00:00
.application--wrap {
background-color: mc('grey', '900');
}
&-title-input input {
text-align: center;
}
2018-09-16 22:42:41 +00:00
}
2018-07-15 23:16:19 +00:00
.atom-spinner.is-inline {
display: inline-block;
}
2018-02-11 05:20:17 +00:00
</style>