feat: page edit + setup instructions

parent 7128b160
......@@ -141,8 +141,8 @@ export default {
default: null
},
pageId: {
type: Number,
default: 0
type: String,
default: ''
},
checkoutDate: {
type: String,
......@@ -369,33 +369,32 @@ export default {
// -> UPDATE EXISTING PAGE
// --------------------------------------------
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'))
}
// 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'))
// }
let resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$id: Int!
$id: UUID!
$content: String
$description: String
$editor: String
$isPrivate: Boolean
$isPublished: Boolean
$locale: String
$path: String
......@@ -406,32 +405,27 @@ export default {
$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
) {
operation {
succeeded
errorCode
slug
message
}
page {
updatedAt
}
updatePage(
id: $id
content: $content
description: $description
editor: $editor
isPublished: $isPublished
locale: $locale
path: $path
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
scriptCss: $scriptCss
scriptJs: $scriptJs
tags: $tags
title: $title
) {
operation {
succeeded
message
}
page {
updatedAt
}
}
}
......@@ -442,7 +436,6 @@ export default {
description: this.$store.get('page/description'),
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') || '',
......@@ -453,7 +446,7 @@ export default {
title: this.$store.get('page/title')
}
})
resp = _.get(resp, 'data.pages.update', {})
resp = _.get(resp, 'data.updatePage', {})
if (_.get(resp, 'operation.succeeded')) {
this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
this.isConflict = false
......@@ -547,30 +540,30 @@ export default {
styl.appendChild(document.createTextNode(css))
}
}, 1000)
},
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,
checkoutDate: this.checkoutDateActive
}
},
update: (data) => _.cloneDeep(data.pages.checkConflicts),
skip () {
return this.mode === 'create' || this.isSaving || !this.isDirty
}
}
}
// 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,
// checkoutDate: this.checkoutDateActive
// }
// },
// update: (data) => _.cloneDeep(data.pages.checkConflicts),
// skip () {
// return this.mode === 'create' || this.isSaving || !this.isDirty
// }
// }
// }
}
</script>
......
......@@ -238,51 +238,6 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
js: ''
}
}
// -> From Template
if (req.query.from && tmplCreateRegex.test(req.query.from)) {
let tmplPageId = 0
let tmplVersionId = 0
if (req.query.from.indexOf(',')) {
const q = req.query.from.split(',')
tmplPageId = _.toSafeInteger(q[0])
tmplVersionId = _.toSafeInteger(q[1])
} else {
tmplPageId = _.toSafeInteger(req.query.from)
}
if (tmplVersionId > 0) {
// -> From Page Version
const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId })
if (!pageVersion) {
_.set(res.locals, 'pageMeta.title', 'Page Not Found')
return res.status(404).render('notfound', { action: 'template' })
}
if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'sourceVersion' })
}
page.content = Buffer.from(pageVersion.content).toString('base64')
page.editorKey = pageVersion.editor
page.title = pageVersion.title
page.description = pageVersion.description
} else {
// -> From Page Live
const pageOriginal = await WIKI.db.pages.query().findById(tmplPageId)
if (!pageOriginal) {
_.set(res.locals, 'pageMeta.title', 'Page Not Found')
return res.status(404).render('notfound', { action: 'template' })
}
if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'source' })
}
page.content = Buffer.from(pageOriginal.content).toString('base64')
page.editorKey = pageOriginal.editorKey
page.title = pageOriginal.title
page.description = pageOriginal.description
}
}
}
res.render('editor', { page, injectCode, effectivePermissions })
......
......@@ -9,7 +9,7 @@ const fs = require('fs')
*/
const packages = {
'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)
'twemoji': path.join(WIKI.ROOTPATH, `assets-legacy/svg/twemoji.asar`)
}
module.exports = {
......
......@@ -603,7 +603,8 @@ exports.up = async knex => {
auth: {
[authModuleId]: {
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
mustChangePwd: !process.env.ADMIN_PASS,
mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented
// mustChangePwd: !process.env.ADMIN_PASS,
restrictLogin: false,
tfaRequired: false,
tfaSecret: ''
......
const _ = require('lodash')
const graphHelper = require('../../helpers/graph')
const pageHelper = require('../../helpers/page')
module.exports = {
Query: {
......@@ -139,7 +140,7 @@ module.exports = {
return results
},
/**
* FETCH SINGLE PAGE
* FETCH SINGLE PAGE BY ID
*/
async pageById (obj, args, context, info) {
let page = await WIKI.db.pages.getPageFromDb(args.id)
......@@ -161,6 +162,22 @@ module.exports = {
}
},
/**
* FETCH SINGLE PAGE BY PATH
*/
async pageByPath (obj, args, context, info) {
const pageArgs = pageHelper.parsePath(args.path)
let page = await WIKI.db.pages.getPageFromDb(pageArgs)
if (page) {
return {
...page,
locale: page.localeCode,
editor: page.editorKey
}
} else {
throw new Error('ERR_PAGE_NOT_FOUND')
}
},
/**
* FETCH TAGS
*/
async tags (obj, args, context, info) {
......@@ -366,7 +383,7 @@ module.exports = {
user: context.req.user
})
return {
responseResult: graphHelper.generateSuccess('Page created successfully.'),
operation: graphHelper.generateSuccess('Page created successfully.'),
page
}
} catch (err) {
......@@ -383,7 +400,7 @@ module.exports = {
user: context.req.user
})
return {
responseResult: graphHelper.generateSuccess('Page has been updated.'),
operation: graphHelper.generateSuccess('Page has been updated.'),
page
}
} catch (err) {
......
......@@ -34,6 +34,10 @@ extend type Query {
id: Int!
): Page
pageByPath(
path: String!
): Page
tags: [PageTag]!
searchTags(
......@@ -80,7 +84,7 @@ extend type Mutation {
): PageResponse
updatePage(
id: Int!
id: UUID!
content: String
description: String
editor: String
......@@ -158,7 +162,7 @@ type PageMigrationResponse {
}
type Page {
id: Int
id: UUID
path: String
hash: String
title: String
......
......@@ -19,7 +19,7 @@ module.exports = class PageHistory extends Model {
hash: {type: 'string'},
title: {type: 'string'},
description: {type: 'string'},
isPublished: {type: 'boolean'},
publishState: {type: 'string'},
publishStartDate: {type: 'string'},
publishEndDate: {type: 'string'},
content: {type: 'string'},
......@@ -60,14 +60,6 @@ module.exports = class PageHistory extends Model {
to: 'users.id'
}
},
editor: {
relation: Model.BelongsToOneRelation,
modelClass: require('./editors'),
join: {
from: 'pageHistory.editorKey',
to: 'editors.key'
}
},
locale: {
relation: Model.BelongsToOneRelation,
modelClass: require('./locales'),
......@@ -89,18 +81,18 @@ module.exports = class PageHistory extends Model {
static async addVersion(opts) {
await WIKI.db.pageHistory.query().insert({
pageId: opts.id,
siteId: opts.siteId,
authorId: opts.authorId,
content: opts.content,
contentType: opts.contentType,
description: opts.description,
editorKey: opts.editorKey,
editor: opts.editor,
hash: opts.hash,
isPrivate: (opts.isPrivate === true || opts.isPrivate === 1),
isPublished: (opts.isPublished === true || opts.isPublished === 1),
publishState: opts.publishState,
localeCode: opts.localeCode,
path: opts.path,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate?.toISO(),
title: opts.title,
action: opts.action || 'updated',
versionDate: opts.versionDate
......@@ -116,7 +108,6 @@ module.exports = class PageHistory extends Model {
'pageHistory.path',
'pageHistory.title',
'pageHistory.description',
'pageHistory.isPrivate',
'pageHistory.isPublished',
'pageHistory.publishStartDate',
'pageHistory.publishEndDate',
......
......@@ -421,8 +421,8 @@ module.exports = class Page extends Model {
content: opts.content,
description: opts.description,
publishState: opts.publishState,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate?.toISO(),
title: opts.title,
extra: JSON.stringify({
...ogPage.extra,
......@@ -439,18 +439,18 @@ module.exports = class Page extends Model {
await WIKI.db.pages.renderPage(page)
WIKI.events.outbound.emit('deletePageFromCache', page.hash)
// -> Update Search Index
const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
await WIKI.data.searchEngine.updated(page)
// // -> Update Search Index
// const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
// page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
// await WIKI.data.searchEngine.updated(page)
// -> Update on Storage
if (!opts.skipStorage) {
await WIKI.db.storage.pageEvent({
event: 'updated',
page
})
}
// if (!opts.skipStorage) {
// await WIKI.db.storage.pageEvent({
// event: 'updated',
// page
// })
// }
// -> Perform move?
if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {
......
......@@ -40,28 +40,20 @@ html(lang=siteConfig.lang)
//- CSS
link(
type='text/css'
rel='stylesheet'
href='/_assets-legacy/css/app.629ebe3c082227dbee31.css'
)
//- JS
script(
type='text/javascript'
src='/_assets-legacy/js/runtime.js?1664769154'
src='/_assets-legacy/js/runtime.js'
)
script(
type='text/javascript'
src='/_assets-legacy/js/app.js?1664769154'
src='/_assets-legacy/js/app.js'
)
......
......@@ -7,7 +7,7 @@ block head
block body
#root
editor(
:page-id=page.id
page-id=page.id
locale=page.localeCode
path=page.path
title=page.title
......
......@@ -5,18 +5,12 @@ enableTelemetry: false
nodeLinker: node-modules
packageExtensions:
'@quasar/vite-plugin@*':
dependencies:
'quasar': '*'
'rollup-plugin-visualizer@*':
dependencies:
'rollup': '*'
'v-network-graph@*':
dependencies:
'd3-force': '*'
'@intlify/vite-plugin-vue-i18n@*':
dependencies:
'vite': '*'
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
......
......@@ -77,6 +77,7 @@ module.exports = configure(function (/* ctx */) {
extendViteConf (viteConf) {
viteConf.build.assetsDir = '_assets'
// viteConf.resolve.alias.vue = '/workspace/ux/node_modules/vue/dist/vue.esm-bundler.js'
// viteConf.build.rollupOptions = {
// ...viteConf.build.rollupOptions ?? {},
// external: [
......
......@@ -5,7 +5,7 @@ q-item-section(avatar)
:text-color='avatarTextColor'
font-size='14px'
rounded
:style='hueRotate !== 0 ? `filter: hue-rotate(` + hueRotate + `deg)` : ``'
:style='props.hueRotate !== 0 ? `filter: hue-rotate(` + props.hueRotate + `deg)` : ``'
)
q-badge(
v-if='indicatorDot'
......@@ -13,57 +13,57 @@ q-item-section(avatar)
:color='indicatorDot'
floating
)
q-tooltip(v-if='indicatorText') {{indicatorText}}
q-tooltip(v-if='props.indicatorText') {{props.indicatorText}}
q-icon(
v-if='!textMode'
:name='`img:/_assets/icons/ultraviolet-` + icon + `.svg`'
size='sm'
)
span.text-uppercase(v-else) {{text}}
span.text-uppercase(v-else) {{props.text}}
</template>
<script>
export default {
name: 'BlueprintIcon',
props: {
icon: {
type: String,
default: ''
},
dark: {
type: Boolean,
default: false
},
indicator: {
type: String,
default: null
},
indicatorText: {
type: String,
default: null
},
hueRotate: {
type: Number,
default: 0
},
text: {
type: String,
default: null
}
<script setup>
import { computed } from 'vue'
import { useQuasar } from 'quasar'
const props = defineProps({
icon: {
type: String,
default: ''
},
dark: {
type: Boolean,
default: false
},
indicator: {
type: String,
default: null
},
indicatorText: {
type: String,
default: null
},
data () {
return {
imgPath: null
}
hueRotate: {
type: Number,
default: 0
},
computed: {
textMode () { return this.text !== null },
avatarBgColor () { return this.$q.dark.isActive || this.dark ? 'dark-4' : 'blue-1' },
avatarTextColor () { return this.$q.dark.isActive || this.dark ? 'white' : 'blue-7' },
indicatorDot () {
if (this.indicator === null) { return null }
return (this.indicator === '') ? 'pink' : this.indicator
}
text: {
type: String,
default: null
}
}
})
// QUASAR
const $q = useQuasar()
// COMPUTED
const textMode = computed(() => { return props.text !== null })
const avatarBgColor = computed(() => { return $q.dark.isActive || props.dark ? 'dark-4' : 'blue-1' })
const avatarTextColor = computed(() => { return $q.dark.isActive || props.dark ? 'white' : 'blue-7' })
const indicatorDot = computed(() => {
if (props.indicator === null) { return null }
return (props.indicator === '') ? 'pink' : props.indicator
})
</script>
<template lang="pug">
q-card.icon-picker(flat, style='width: 400px;')
q-tabs.text-primary(
v-model='currentTab'
v-model='state.currentTab'
no-caps
inline-label
)
......@@ -17,12 +17,12 @@ q-card.icon-picker(flat, style='width: 400px;')
)
q-separator
q-tab-panels(
v-model='currentTab'
v-model='state.currentTab'
)
q-tab-panel(name='icon')
q-select(
:options='iconPacks'
v-model='selPack'
v-model='state.selPack'
emit-value
map-options
outlined
......@@ -52,7 +52,7 @@ q-card.icon-picker(flat, style='width: 400px;')
size='sm'
) {{scope.opt.subset.toUpperCase()}}
q-input.q-mt-md(
v-model='selIcon'
v-model='state.selIcon'
outlined
label='Icon Name'
dense
......@@ -96,7 +96,7 @@ q-card.icon-picker(flat, style='width: 400px;')
q-img(
transition='jump-down'
:ratio='1'
:src='imgPath'
:src='state.imgPath'
)
q-separator
q-card-actions
......@@ -118,67 +118,80 @@ q-card.icon-picker(flat, style='width: 400px;')
)
</template>
<script>
<script setup>
import { find } from 'lodash-es'
import { computed, onMounted, reactive } from 'vue'
export default {
props: {
value: {
type: String,
required: true
}
},
data () {
return {
currentTab: 'icon',
selPack: 'las',
selIcon: '',
imgPath: 'https://placeimg.com/64/64/nature',
iconPacks: [
{ value: 'las', label: 'Line Awesome (solid)', name: 'Line Awesome', subset: 'solid', prefix: 'las la-', reference: 'https://icons8.com/line-awesome' },
{ value: 'lab', label: 'Line Awesome (brands)', name: 'Line Awesome', subset: 'brands', prefix: 'lab la-', reference: 'https://icons8.com/line-awesome' },
{ value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' },
{ value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'far', label: 'Font Awesome (regular)', name: 'Font Awesome', subset: 'regular', prefix: 'far fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fal', label: 'Font Awesome (light)', name: 'Font Awesome', subset: 'light', prefix: 'fal fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fad', label: 'Font Awesome (duotone)', name: 'Font Awesome', subset: 'duotone', prefix: 'fad fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fab', label: 'Font Awesome (brands)', name: 'Font Awesome', subset: 'brands', prefix: 'fab fa-', reference: 'https://fontawesome.com/icons' }
]
}
},
computed: {
iconName () {
return find(this.iconPacks, ['value', this.selPack]).prefix + this.selIcon
},
iconPackRefWebsite () {
return find(this.iconPacks, ['value', this.selPack]).reference
}
},
mounted () {
if (this.value?.startsWith('img:')) {
this.currentTab = 'img'
this.imgPath = this.value.substring(4)
} else {
this.currentTab = 'icon'
for (const pack of this.iconPacks) {
if (this.value?.startsWith(pack.prefix)) {
this.selPack = pack.value
this.selIcon = this.value.substring(pack.prefix.length)
break
}
}
}
},
methods: {
apply () {
if (this.currentTab === 'img') {
this.$emit('input', `img:${this.imgPath}`)
} else {
this.$emit('input', this.iconName)
// PROPS
const props = defineProps({
value: {
type: String,
required: true
}
})
// EMITS
const emit = defineEmits(['input'])
// DATA
const state = reactive({
currentTab: 'icon',
selPack: 'las',
selIcon: '',
imgPath: 'https://placeimg.com/64/64/nature'
})
const iconPacks = [
{ value: 'las', label: 'Line Awesome (solid)', name: 'Line Awesome', subset: 'solid', prefix: 'las la-', reference: 'https://icons8.com/line-awesome' },
{ value: 'lab', label: 'Line Awesome (brands)', name: 'Line Awesome', subset: 'brands', prefix: 'lab la-', reference: 'https://icons8.com/line-awesome' },
{ value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' },
{ value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'far', label: 'Font Awesome (regular)', name: 'Font Awesome', subset: 'regular', prefix: 'far fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fal', label: 'Font Awesome (light)', name: 'Font Awesome', subset: 'light', prefix: 'fal fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fad', label: 'Font Awesome (duotone)', name: 'Font Awesome', subset: 'duotone', prefix: 'fad fa-', reference: 'https://fontawesome.com/icons' },
{ value: 'fab', label: 'Font Awesome (brands)', name: 'Font Awesome', subset: 'brands', prefix: 'fab fa-', reference: 'https://fontawesome.com/icons' }
]
// COMPUTED
const iconName = computed(() => {
return find(iconPacks, ['value', state.selPack]).prefix + state.selIcon
})
const iconPackRefWebsite = computed(() => {
return find(iconPacks, ['value', state.selPack]).reference
})
// METHODS
function apply () {
if (state.currentTab === 'img') {
emit('input', `img:${state.imgPath}`)
} else {
emit('input', state.iconName)
}
}
// MOUNTED
onMounted(() => {
if (props.value?.startsWith('img:')) {
state.currentTab = 'img'
state.imgPath = props.value.substring(4)
} else {
state.currentTab = 'icon'
for (const pack of iconPacks) {
if (props.value?.startsWith(pack.prefix)) {
state.selPack = pack.value
state.selIcon = props.value.substring(pack.prefix.length)
break
}
}
}
}
})
</script>
<style lang="scss">
......
<template lang="pug">
q-card.page-relation-dialog(style='width: 500px;')
q-toolbar.bg-primary.text-white
.text-subtitle2(v-if='isEditMode') {{$t('editor.pageRel.titleEdit')}}
.text-subtitle2(v-else) {{$t('editor.pageRel.title')}}
.text-subtitle2(v-if='isEditMode') {{t('editor.pageRel.titleEdit')}}
.text-subtitle2(v-else) {{t('editor.pageRel.title')}}
q-card-section
.text-overline {{$t('editor.pageRel.position')}}
.text-overline {{t('editor.pageRel.position')}}
q-form.q-gutter-md.q-pt-md
div
q-btn-toggle(
v-model='pos'
v-model='state.pos'
push
glossy
no-caps
toggle-color='primary'
:options=`[
{ label: $t('editor.pageRel.left'), value: 'left' },
{ label: $t('editor.pageRel.center'), value: 'center' },
{ label: $t('editor.pageRel.right'), value: 'right' }
{ label: t('editor.pageRel.left'), value: 'left' },
{ label: t('editor.pageRel.center'), value: 'center' },
{ label: t('editor.pageRel.right'), value: 'right' }
]`
)
.text-overline {{$t('editor.pageRel.button')}}
.text-overline {{t('editor.pageRel.button')}}
q-input(
ref='iptRelLabel'
outlined
dense
:label='$t(`editor.pageRel.label`)'
v-model='label'
:label='t(`editor.pageRel.label`)'
v-model='state.label'
)
template(v-if='pos !== `center`')
template(v-if='state.pos !== `center`')
q-input(
outlined
dense
:label='$t(`editor.pageRel.caption`)'
v-model='caption'
:label='t(`editor.pageRel.caption`)'
v-model='state.caption'
)
q-btn.rounded-borders(
:label='$t(`editor.pageRel.selectIcon`)'
:label='t(`editor.pageRel.selectIcon`)'
color='primary'
outline
)
q-menu(content-class='shadow-7')
icon-picker-dialog(v-model='icon')
.text-overline {{$t('editor.pageRel.target')}}
icon-picker-dialog(v-model='state.icon')
.text-overline {{t('editor.pageRel.target')}}
q-btn.rounded-borders(
:label='$t(`editor.pageRel.selectPage`)'
:label='t(`editor.pageRel.selectPage`)'
color='primary'
outline
)
.text-overline {{$t('editor.pageRel.preview')}}
.text-overline {{t('editor.pageRel.preview')}}
q-btn(
v-if='pos === `left`'
v-if='state.pos === `left`'
padding='sm md'
outline
:icon='icon'
:icon='state.icon'
no-caps
color='primary'
)
.column.text-left.q-pl-md
.text-body2: strong {{label}}
.text-caption {{caption}}
.text-body2: strong {{state.label}}
.text-caption {{state.caption}}
q-btn.full-width(
v-else-if='pos === `center`'
:label='label'
v-else-if='state.pos === `center`'
:label='state.label'
color='primary'
flat
no-caps
:icon='icon'
:icon='state.icon'
)
q-btn(
v-else-if='pos === `right`'
v-else-if='state.pos === `right`'
padding='sm md'
outline
:icon-right='icon'
:icon-right='state.icon'
no-caps
color='primary'
)
.column.text-left.q-pr-md
.text-body2: strong {{label}}
.text-caption {{caption}}
.text-body2: strong {{state.label}}
.text-caption {{state.caption}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
icon='las la-times'
:label='$t(`common.actions.discard`)'
:label='t(`common.actions.discard`)'
color='grey-7'
padding='xs md'
v-close-popup
......@@ -92,7 +92,7 @@ q-card.page-relation-dialog(style='width: 500px;')
v-if='isEditMode'
:disabled='!canSubmit'
icon='las la-check'
:label='$t(`common.actions.save`)'
:label='t(`common.actions.save`)'
unelevated
color='primary'
padding='xs md'
......@@ -103,7 +103,7 @@ q-card.page-relation-dialog(style='width: 500px;')
v-else
:disabled='!canSubmit'
icon='las la-plus'
:label='$t(`common.actions.create`)'
:label='t(`common.actions.create`)'
unelevated
color='primary'
padding='xs md'
......@@ -112,99 +112,127 @@ q-card.page-relation-dialog(style='width: 500px;')
)
</template>
<script>
<script setup>
import { v4 as uuid } from 'uuid'
import { cloneDeep, find } from 'lodash-es'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import IconPickerDialog from './IconPickerDialog.vue'
export default {
components: {
IconPickerDialog
},
props: {
editId: {
type: String,
default: null
}
},
data () {
return {
pos: 'left',
label: '',
caption: '',
icon: 'las la-arrow-left',
target: ''
}
},
computed: {
canSubmit () {
return this.label.length > 0
},
isEditMode () {
return Boolean(this.editId)
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// PROPS
const props = defineProps({
editId: {
type: String,
default: null
}
})
// QUASAR
const $q = useQuasar()
// STORES
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
pos: 'left',
label: '',
caption: '',
icon: 'las la-arrow-left',
target: ''
})
// REFS
const iptRelLabel = ref(null)
// COMPUTED
const canSubmit = computed(() => state.label.length > 0)
const isEditMode = computed(() => Boolean(props.editId))
// WATCHERS
watch(() => state.pos, (newValue) => {
switch (newValue) {
case 'left': {
state.icon = 'las la-arrow-left'
break
}
},
watch: {
pos (newValue) {
switch (newValue) {
case 'left': {
this.icon = 'las la-arrow-left'
break
}
case 'center': {
this.icon = 'las la-book'
break
}
case 'right': {
this.icon = 'las la-arrow-right'
break
}
}
case 'center': {
state.icon = 'las la-book'
break
}
},
mounted () {
if (this.editId) {
const rel = find(this.$store.get('page/relations'), ['id', this.editId])
if (rel) {
this.pos = rel.position
this.label = rel.label
this.caption = rel.caption || ''
this.icon = rel.icon
this.target = rel.target
}
case 'right': {
state.icon = 'las la-arrow-right'
break
}
this.$nextTick(() => {
this.$refs.iptRelLabel.focus()
})
},
methods: {
create () {
this.$store.set('page/relations', [
...this.$store.get('page/relations'),
{
id: uuid(),
position: this.pos,
label: this.label,
...(this.pos !== 'center' ? { caption: this.caption } : {}),
icon: this.icon,
target: this.target
}
])
},
persist () {
const rels = cloneDeep(this.$store.get('page/relations'))
for (const rel of rels) {
if (rel.id === this.editId) {
rel.position = this.pos
rel.label = this.label
rel.caption = this.caption
rel.icon = this.icon
rel.target = this.target
}
}
})
// METHODS
function create () {
pageStore.$patch({
relations: [
...pageStore.relations,
{
id: uuid(),
position: state.pos,
label: state.label,
...(state.pos !== 'center' ? { caption: state.caption } : {}),
icon: state.icon,
target: state.target
}
this.$store.set('page/relations', rels)
]
})
}
function persist () {
const rels = cloneDeep(pageStore.relations)
for (const rel of rels) {
if (rel.id === state.editId) {
rel.position = state.pos
rel.label = state.label
rel.caption = state.caption
rel.icon = state.icon
rel.target = state.target
}
}
pageStore.$patch({
relations: rels
})
}
// MOUNTED
onMounted(() => {
if (props.editId) {
const rel = find(pageStore.relations, ['id', props.editId])
if (rel) {
state.pos = rel.position
state.label = rel.label
state.caption = rel.caption || ''
state.icon = rel.icon
state.target = rel.target
}
}
nextTick(() => {
iptRelLabel.value.focus()
})
})
</script>
......@@ -222,7 +222,9 @@ body::-webkit-scrollbar-thumb {
@import './animation.scss';
@import 'v-network-graph/lib/style.css'
@import 'v-network-graph/lib/style.css';
@import './page-contents.scss';
// @import '~codemirror/lib/codemirror.css';
// @import '~codemirror/theme/elegant.css';
......
.page-contents {
color: #424242;
font-size: 14px;
// ---------------------------------
// HEADERS
// ---------------------------------
h1, h2, h3, h4, h5, h6 {
padding: 0;
margin: 0;
position: relative;
line-height: normal;
&:first-child {
padding-top: 0;
}
&:hover {
.toc-anchor {
display: block;
}
}
}
* + h1, * + h2, * + h3 {
border-top: 1px solid #DDD;
}
h1 {
font-size: 3em;
font-weight: 500;
padding: 12px 0;
}
h2 {
font-size: 2.4em;
padding: 12px 0;
}
h3 {
font-size: 2em;
padding: 12px 0;
}
h4 {
font-size: 1.75em;
}
h5 {
font-size: 1.5em;
}
h6 {
font-size: 1.25em;
}
.toc-anchor {
display: none;
position: absolute;
right: 1rem;
bottom: .5rem;
font-size: 1.25rem;
text-decoration: none;
color: #666;
}
}
......@@ -18,13 +18,13 @@ q-page.column
:icon='brd.icon'
:label='brd.title'
:aria-label='brd.title'
:to='getFullPath(brd)'
:to='brd.path'
)
.col-auto.flex.items-center.justify-end
template(v-if='!pageStore.isPublished')
.text-caption.text-accent: strong Unpublished
q-separator.q-mx-sm(vertical)
.text-caption.text-grey-6 Last modified on #[strong September 5th, 2020]
.text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
.page-header.row
//- PAGE ICON
.col-auto.q-pl-md.flex.items-center
......@@ -90,7 +90,7 @@ q-page.column
style='height: 100%;'
)
.q-pa-md
div(v-html='pageStore.render')
.page-contents(v-html='pageStore.render')
template(v-if='pageStore.relations && pageStore.relations.length > 0')
q-separator.q-my-lg
.row.align-center
......@@ -288,14 +288,14 @@ q-page.column
transition-hide='jump-right'
class='floating-sidepanel'
)
component(:is='state.sideDialogComponent')
component(:is='sideDialogs[state.sideDialogComponent]')
q-dialog(
v-model='state.showGlobalDialog'
transition-show='jump-up'
transition-hide='jump-down'
)
component(:is='state.globalDialogComponent')
component(:is='globalDialogs[state.globalDialogComponent]')
</template>
<script setup>
......@@ -303,17 +303,23 @@ import { useMeta, useQuasar, setCssVar } from 'quasar'
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from '../stores/site'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS
import SocialSharingMenu from '../components/SocialSharingMenu.vue'
import PageDataDialog from '../components/PageDataDialog.vue'
import PageTags from '../components/PageTags.vue'
import PagePropertiesDialog from '../components/PagePropertiesDialog.vue'
import PageSaveDialog from '../components/PageSaveDialog.vue'
const sideDialogs = {
PageDataDialog: defineAsyncComponent(() => import('../components/PageDataDialog.vue')),
PagePropertiesDialog: defineAsyncComponent(() => import('../components/PagePropertiesDialog.vue'))
}
const globalDialogs = {
PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
}
// QUASAR
......@@ -441,22 +447,36 @@ const editUrl = computed(() => {
pagePath += !pageStore.path ? 'home' : pageStore.path
return `/_edit/${pagePath}`
})
const lastModified = computed(() => {
return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
})
// WATCHERS
watch(() => route.path, async (newValue) => {
if (newValue.startsWith('/_')) { return }
try {
await pageStore.pageLoad({ path: newValue })
} catch (err) {
if (err.message === 'ERR_PAGE_NOT_FOUND') {
$q.notify({
type: 'negative',
message: 'This page does not exist (yet)!'
})
} else {
$q.notify({
type: 'negative',
message: err.message
})
}
}
}, { immediate: true })
watch(() => state.toc, refreshTocExpanded)
watch(() => pageStore.tocDepth, refreshTocExpanded)
// METHODS
function getFullPath ({ locale, path }) {
if (siteStore.useLocales) {
return `/${locale}/${path}`
} else {
return `/${path}`
}
}
function togglePageProperties () {
state.sideDialogComponent = 'PagePropertiesDialog'
state.showSideDialog = true
......
import { defineStore } from 'pinia'
import gql from 'graphql-tag'
import { cloneDeep, last, transform } from 'lodash-es'
import { useSiteStore } from './site'
export const usePageStore = defineStore('page', {
state: () => ({
isLoading: true,
mode: 'view',
editor: 'wysiwyg',
editorMode: 'edit',
id: 0,
authorId: 0,
authorName: 'Unknown',
authorName: '',
createdAt: '',
description: 'How to install Wiki.js on Ubuntu 18.04 / 20.04',
description: '',
isPublished: true,
showInTree: true,
locale: 'en',
path: '',
publishEndDate: '',
publishStartDate: '',
tags: ['cities', 'canada'],
title: 'Ubuntu',
icon: 'lab la-empire',
tags: [],
title: '',
icon: 'las la-file-alt',
updatedAt: '',
relations: [],
scriptJsLoad: '',
......@@ -35,20 +40,20 @@ export const usePageStore = defineStore('page', {
max: 2
},
breadcrumbs: [
{
id: 1,
title: 'Installation',
icon: 'las la-file-alt',
locale: 'en',
path: 'installation'
},
{
id: 2,
title: 'Ubuntu',
icon: 'lab la-ubuntu',
locale: 'en',
path: 'installation/ubuntu'
}
// {
// id: 1,
// title: 'Installation',
// icon: 'las la-file-alt',
// locale: 'en',
// path: 'installation'
// },
// {
// id: 2,
// title: 'Ubuntu',
// icon: 'lab la-ubuntu',
// locale: 'en',
// path: 'installation/ubuntu'
// }
],
effectivePermissions: {
comments: {
......@@ -75,11 +80,62 @@ export const usePageStore = defineStore('page', {
},
commentsCount: 0,
content: '',
render: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
render: ''
}),
getters: {},
actions: {
/**
* PAGE - LOAD
*/
async pageLoad ({ path, id }) {
const siteStore = useSiteStore()
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query loadPage (
$path: String!
) {
pageByPath(
path: $path
) {
id
title
description
path
locale
updatedAt
render
}
}
`,
variables: {
path
},
fetchPolicy: 'network-only'
})
const pageData = cloneDeep(resp?.data?.pageByPath ?? {})
if (!pageData?.id) {
throw new Error('ERR_PAGE_NOT_FOUND')
}
const pathPrefix = siteStore.useLocales ? `/${pageData.locale}` : ''
this.$patch({
...pageData,
breadcrumbs: transform(pageData.path.split('/'), (result, value, key) => {
result.push({
id: key,
title: value,
icon: 'las la-file-alt',
locale: 'en',
path: (last(result)?.path || pathPrefix) + `/${value}`
})
}, [])
})
} catch (err) {
console.warn(err)
throw err
}
},
/**
* PAGE - CREATE
*/
pageCreate ({ editor, locale, path }) {
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment