feat: page edit + setup instructions

parent 7128b160
...@@ -141,8 +141,8 @@ export default { ...@@ -141,8 +141,8 @@ export default {
default: null default: null
}, },
pageId: { pageId: {
type: Number, type: String,
default: 0 default: ''
}, },
checkoutDate: { checkoutDate: {
type: String, type: String,
...@@ -369,33 +369,32 @@ export default { ...@@ -369,33 +369,32 @@ export default {
// -> UPDATE EXISTING PAGE // -> UPDATE EXISTING PAGE
// -------------------------------------------- // --------------------------------------------
const conflictResp = await this.$apollo.query({ // const conflictResp = await this.$apollo.query({
query: gql` // query: gql`
query ($id: Int!, $checkoutDate: Date!) { // query ($id: Int!, $checkoutDate: Date!) {
pages { // pages {
checkConflicts(id: $id, checkoutDate: $checkoutDate) // checkConflicts(id: $id, checkoutDate: $checkoutDate)
} // }
} // }
`, // `,
fetchPolicy: 'network-only', // fetchPolicy: 'network-only',
variables: { // variables: {
id: this.pageId, // id: this.pageId,
checkoutDate: this.checkoutDateActive // checkoutDate: this.checkoutDateActive
} // }
}) // })
if (_.get(conflictResp, 'data.pages.checkConflicts', false)) { // if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
this.$root.$emit('saveConflict') // this.$root.$emit('saveConflict')
throw new Error(this.$t('editor:conflict.warning')) // throw new Error(this.$t('editor:conflict.warning'))
} // }
let resp = await this.$apollo.mutate({ let resp = await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation ( mutation (
$id: Int! $id: UUID!
$content: String $content: String
$description: String $description: String
$editor: String $editor: String
$isPrivate: Boolean
$isPublished: Boolean $isPublished: Boolean
$locale: String $locale: String
$path: String $path: String
...@@ -406,32 +405,27 @@ export default { ...@@ -406,32 +405,27 @@ export default {
$tags: [String] $tags: [String]
$title: String $title: String
) { ) {
pages { updatePage(
update( id: $id
id: $id content: $content
content: $content description: $description
description: $description editor: $editor
editor: $editor isPublished: $isPublished
isPrivate: $isPrivate locale: $locale
isPublished: $isPublished path: $path
locale: $locale publishEndDate: $publishEndDate
path: $path publishStartDate: $publishStartDate
publishEndDate: $publishEndDate scriptCss: $scriptCss
publishStartDate: $publishStartDate scriptJs: $scriptJs
scriptCss: $scriptCss tags: $tags
scriptJs: $scriptJs title: $title
tags: $tags ) {
title: $title operation {
) { succeeded
operation { message
succeeded }
errorCode page {
slug updatedAt
message
}
page {
updatedAt
}
} }
} }
} }
...@@ -442,7 +436,6 @@ export default { ...@@ -442,7 +436,6 @@ export default {
description: this.$store.get('page/description'), description: this.$store.get('page/description'),
editor: this.$store.get('editor/editorKey'), editor: this.$store.get('editor/editorKey'),
locale: this.$store.get('page/locale'), locale: this.$store.get('page/locale'),
isPrivate: false,
isPublished: this.$store.get('page/isPublished'), isPublished: this.$store.get('page/isPublished'),
path: this.$store.get('page/path'), path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate') || '', publishEndDate: this.$store.get('page/publishEndDate') || '',
...@@ -453,7 +446,7 @@ export default { ...@@ -453,7 +446,7 @@ export default {
title: this.$store.get('page/title') title: this.$store.get('page/title')
} }
}) })
resp = _.get(resp, 'data.pages.update', {}) resp = _.get(resp, 'data.updatePage', {})
if (_.get(resp, 'operation.succeeded')) { if (_.get(resp, 'operation.succeeded')) {
this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive) this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
this.isConflict = false this.isConflict = false
...@@ -547,30 +540,30 @@ export default { ...@@ -547,30 +540,30 @@ export default {
styl.appendChild(document.createTextNode(css)) styl.appendChild(document.createTextNode(css))
} }
}, 1000) }, 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> </script>
......
...@@ -238,51 +238,6 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => { ...@@ -238,51 +238,6 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
js: '' 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 }) res.render('editor', { page, injectCode, effectivePermissions })
......
...@@ -9,7 +9,7 @@ const fs = require('fs') ...@@ -9,7 +9,7 @@ const fs = require('fs')
*/ */
const packages = { const packages = {
'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`) 'twemoji': path.join(WIKI.ROOTPATH, `assets-legacy/svg/twemoji.asar`)
} }
module.exports = { module.exports = {
......
...@@ -603,7 +603,8 @@ exports.up = async knex => { ...@@ -603,7 +603,8 @@ exports.up = async knex => {
auth: { auth: {
[authModuleId]: { [authModuleId]: {
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12), 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, restrictLogin: false,
tfaRequired: false, tfaRequired: false,
tfaSecret: '' tfaSecret: ''
......
const _ = require('lodash') const _ = require('lodash')
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
const pageHelper = require('../../helpers/page')
module.exports = { module.exports = {
Query: { Query: {
...@@ -139,7 +140,7 @@ module.exports = { ...@@ -139,7 +140,7 @@ module.exports = {
return results return results
}, },
/** /**
* FETCH SINGLE PAGE * FETCH SINGLE PAGE BY ID
*/ */
async pageById (obj, args, context, info) { async pageById (obj, args, context, info) {
let page = await WIKI.db.pages.getPageFromDb(args.id) let page = await WIKI.db.pages.getPageFromDb(args.id)
...@@ -161,6 +162,22 @@ module.exports = { ...@@ -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 * FETCH TAGS
*/ */
async tags (obj, args, context, info) { async tags (obj, args, context, info) {
...@@ -366,7 +383,7 @@ module.exports = { ...@@ -366,7 +383,7 @@ module.exports = {
user: context.req.user user: context.req.user
}) })
return { return {
responseResult: graphHelper.generateSuccess('Page created successfully.'), operation: graphHelper.generateSuccess('Page created successfully.'),
page page
} }
} catch (err) { } catch (err) {
...@@ -383,7 +400,7 @@ module.exports = { ...@@ -383,7 +400,7 @@ module.exports = {
user: context.req.user user: context.req.user
}) })
return { return {
responseResult: graphHelper.generateSuccess('Page has been updated.'), operation: graphHelper.generateSuccess('Page has been updated.'),
page page
} }
} catch (err) { } catch (err) {
......
...@@ -34,6 +34,10 @@ extend type Query { ...@@ -34,6 +34,10 @@ extend type Query {
id: Int! id: Int!
): Page ): Page
pageByPath(
path: String!
): Page
tags: [PageTag]! tags: [PageTag]!
searchTags( searchTags(
...@@ -80,7 +84,7 @@ extend type Mutation { ...@@ -80,7 +84,7 @@ extend type Mutation {
): PageResponse ): PageResponse
updatePage( updatePage(
id: Int! id: UUID!
content: String content: String
description: String description: String
editor: String editor: String
...@@ -158,7 +162,7 @@ type PageMigrationResponse { ...@@ -158,7 +162,7 @@ type PageMigrationResponse {
} }
type Page { type Page {
id: Int id: UUID
path: String path: String
hash: String hash: String
title: String title: String
......
...@@ -19,7 +19,7 @@ module.exports = class PageHistory extends Model { ...@@ -19,7 +19,7 @@ module.exports = class PageHistory extends Model {
hash: {type: 'string'}, hash: {type: 'string'},
title: {type: 'string'}, title: {type: 'string'},
description: {type: 'string'}, description: {type: 'string'},
isPublished: {type: 'boolean'}, publishState: {type: 'string'},
publishStartDate: {type: 'string'}, publishStartDate: {type: 'string'},
publishEndDate: {type: 'string'}, publishEndDate: {type: 'string'},
content: {type: 'string'}, content: {type: 'string'},
...@@ -60,14 +60,6 @@ module.exports = class PageHistory extends Model { ...@@ -60,14 +60,6 @@ module.exports = class PageHistory extends Model {
to: 'users.id' to: 'users.id'
} }
}, },
editor: {
relation: Model.BelongsToOneRelation,
modelClass: require('./editors'),
join: {
from: 'pageHistory.editorKey',
to: 'editors.key'
}
},
locale: { locale: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: require('./locales'), modelClass: require('./locales'),
...@@ -89,18 +81,18 @@ module.exports = class PageHistory extends Model { ...@@ -89,18 +81,18 @@ module.exports = class PageHistory extends Model {
static async addVersion(opts) { static async addVersion(opts) {
await WIKI.db.pageHistory.query().insert({ await WIKI.db.pageHistory.query().insert({
pageId: opts.id, pageId: opts.id,
siteId: opts.siteId,
authorId: opts.authorId, authorId: opts.authorId,
content: opts.content, content: opts.content,
contentType: opts.contentType, contentType: opts.contentType,
description: opts.description, description: opts.description,
editorKey: opts.editorKey, editor: opts.editor,
hash: opts.hash, hash: opts.hash,
isPrivate: (opts.isPrivate === true || opts.isPrivate === 1), publishState: opts.publishState,
isPublished: (opts.isPublished === true || opts.isPublished === 1),
localeCode: opts.localeCode, localeCode: opts.localeCode,
path: opts.path, path: opts.path,
publishEndDate: opts.publishEndDate || '', publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate || '', publishStartDate: opts.publishStartDate?.toISO(),
title: opts.title, title: opts.title,
action: opts.action || 'updated', action: opts.action || 'updated',
versionDate: opts.versionDate versionDate: opts.versionDate
...@@ -116,7 +108,6 @@ module.exports = class PageHistory extends Model { ...@@ -116,7 +108,6 @@ module.exports = class PageHistory extends Model {
'pageHistory.path', 'pageHistory.path',
'pageHistory.title', 'pageHistory.title',
'pageHistory.description', 'pageHistory.description',
'pageHistory.isPrivate',
'pageHistory.isPublished', 'pageHistory.isPublished',
'pageHistory.publishStartDate', 'pageHistory.publishStartDate',
'pageHistory.publishEndDate', 'pageHistory.publishEndDate',
......
...@@ -421,8 +421,8 @@ module.exports = class Page extends Model { ...@@ -421,8 +421,8 @@ module.exports = class Page extends Model {
content: opts.content, content: opts.content,
description: opts.description, description: opts.description,
publishState: opts.publishState, publishState: opts.publishState,
publishEndDate: opts.publishEndDate || '', publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate || '', publishStartDate: opts.publishStartDate?.toISO(),
title: opts.title, title: opts.title,
extra: JSON.stringify({ extra: JSON.stringify({
...ogPage.extra, ...ogPage.extra,
...@@ -439,18 +439,18 @@ module.exports = class Page extends Model { ...@@ -439,18 +439,18 @@ module.exports = class Page extends Model {
await WIKI.db.pages.renderPage(page) await WIKI.db.pages.renderPage(page)
WIKI.events.outbound.emit('deletePageFromCache', page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash)
// -> Update Search Index // // -> Update Search Index
const pageContents = await WIKI.db.pages.query().findById(page.id).select('render') // const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render) // page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
await WIKI.data.searchEngine.updated(page) // await WIKI.data.searchEngine.updated(page)
// -> Update on Storage // -> Update on Storage
if (!opts.skipStorage) { // if (!opts.skipStorage) {
await WIKI.db.storage.pageEvent({ // await WIKI.db.storage.pageEvent({
event: 'updated', // event: 'updated',
page // page
}) // })
} // }
// -> Perform move? // -> Perform move?
if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) { if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {
......
...@@ -40,28 +40,20 @@ html(lang=siteConfig.lang) ...@@ -40,28 +40,20 @@ html(lang=siteConfig.lang)
//- CSS //- CSS
link(
type='text/css'
rel='stylesheet'
href='/_assets-legacy/css/app.629ebe3c082227dbee31.css'
)
//- JS //- JS
script( script(
type='text/javascript' type='text/javascript'
src='/_assets-legacy/js/runtime.js?1664769154' src='/_assets-legacy/js/runtime.js'
) )
script( script(
type='text/javascript' type='text/javascript'
src='/_assets-legacy/js/app.js?1664769154' src='/_assets-legacy/js/app.js'
) )
......
...@@ -7,7 +7,7 @@ block head ...@@ -7,7 +7,7 @@ block head
block body block body
#root #root
editor( editor(
:page-id=page.id page-id=page.id
locale=page.localeCode locale=page.localeCode
path=page.path path=page.path
title=page.title title=page.title
......
...@@ -5,18 +5,12 @@ enableTelemetry: false ...@@ -5,18 +5,12 @@ enableTelemetry: false
nodeLinker: node-modules nodeLinker: node-modules
packageExtensions: packageExtensions:
'@quasar/vite-plugin@*':
dependencies:
'quasar': '*'
'rollup-plugin-visualizer@*': 'rollup-plugin-visualizer@*':
dependencies: dependencies:
'rollup': '*' 'rollup': '*'
'v-network-graph@*': 'v-network-graph@*':
dependencies: dependencies:
'd3-force': '*' 'd3-force': '*'
'@intlify/vite-plugin-vue-i18n@*':
dependencies:
'vite': '*'
plugins: plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
......
...@@ -77,6 +77,7 @@ module.exports = configure(function (/* ctx */) { ...@@ -77,6 +77,7 @@ module.exports = configure(function (/* ctx */) {
extendViteConf (viteConf) { extendViteConf (viteConf) {
viteConf.build.assetsDir = '_assets' viteConf.build.assetsDir = '_assets'
// viteConf.resolve.alias.vue = '/workspace/ux/node_modules/vue/dist/vue.esm-bundler.js'
// viteConf.build.rollupOptions = { // viteConf.build.rollupOptions = {
// ...viteConf.build.rollupOptions ?? {}, // ...viteConf.build.rollupOptions ?? {},
// external: [ // external: [
......
...@@ -5,7 +5,7 @@ q-item-section(avatar) ...@@ -5,7 +5,7 @@ q-item-section(avatar)
:text-color='avatarTextColor' :text-color='avatarTextColor'
font-size='14px' font-size='14px'
rounded rounded
:style='hueRotate !== 0 ? `filter: hue-rotate(` + hueRotate + `deg)` : ``' :style='props.hueRotate !== 0 ? `filter: hue-rotate(` + props.hueRotate + `deg)` : ``'
) )
q-badge( q-badge(
v-if='indicatorDot' v-if='indicatorDot'
...@@ -13,57 +13,57 @@ q-item-section(avatar) ...@@ -13,57 +13,57 @@ q-item-section(avatar)
:color='indicatorDot' :color='indicatorDot'
floating floating
) )
q-tooltip(v-if='indicatorText') {{indicatorText}} q-tooltip(v-if='props.indicatorText') {{props.indicatorText}}
q-icon( q-icon(
v-if='!textMode' v-if='!textMode'
:name='`img:/_assets/icons/ultraviolet-` + icon + `.svg`' :name='`img:/_assets/icons/ultraviolet-` + icon + `.svg`'
size='sm' size='sm'
) )
span.text-uppercase(v-else) {{text}} span.text-uppercase(v-else) {{props.text}}
</template> </template>
<script> <script setup>
export default { import { computed } from 'vue'
name: 'BlueprintIcon', import { useQuasar } from 'quasar'
props: {
icon: { const props = defineProps({
type: String, icon: {
default: '' type: String,
}, default: ''
dark: { },
type: Boolean, dark: {
default: false type: Boolean,
}, default: false
indicator: { },
type: String, indicator: {
default: null type: String,
}, default: null
indicatorText: { },
type: String, indicatorText: {
default: null type: String,
}, default: null
hueRotate: {
type: Number,
default: 0
},
text: {
type: String,
default: null
}
}, },
data () { hueRotate: {
return { type: Number,
imgPath: null default: 0
}
}, },
computed: { text: {
textMode () { return this.text !== null }, type: String,
avatarBgColor () { return this.$q.dark.isActive || this.dark ? 'dark-4' : 'blue-1' }, default: null
avatarTextColor () { return this.$q.dark.isActive || this.dark ? 'white' : 'blue-7' },
indicatorDot () {
if (this.indicator === null) { return null }
return (this.indicator === '') ? 'pink' : this.indicator
}
} }
} })
// 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> </script>
<template lang="pug"> <template lang="pug">
q-card.icon-picker(flat, style='width: 400px;') q-card.icon-picker(flat, style='width: 400px;')
q-tabs.text-primary( q-tabs.text-primary(
v-model='currentTab' v-model='state.currentTab'
no-caps no-caps
inline-label inline-label
) )
...@@ -17,12 +17,12 @@ q-card.icon-picker(flat, style='width: 400px;') ...@@ -17,12 +17,12 @@ q-card.icon-picker(flat, style='width: 400px;')
) )
q-separator q-separator
q-tab-panels( q-tab-panels(
v-model='currentTab' v-model='state.currentTab'
) )
q-tab-panel(name='icon') q-tab-panel(name='icon')
q-select( q-select(
:options='iconPacks' :options='iconPacks'
v-model='selPack' v-model='state.selPack'
emit-value emit-value
map-options map-options
outlined outlined
...@@ -52,7 +52,7 @@ q-card.icon-picker(flat, style='width: 400px;') ...@@ -52,7 +52,7 @@ q-card.icon-picker(flat, style='width: 400px;')
size='sm' size='sm'
) {{scope.opt.subset.toUpperCase()}} ) {{scope.opt.subset.toUpperCase()}}
q-input.q-mt-md( q-input.q-mt-md(
v-model='selIcon' v-model='state.selIcon'
outlined outlined
label='Icon Name' label='Icon Name'
dense dense
...@@ -96,7 +96,7 @@ q-card.icon-picker(flat, style='width: 400px;') ...@@ -96,7 +96,7 @@ q-card.icon-picker(flat, style='width: 400px;')
q-img( q-img(
transition='jump-down' transition='jump-down'
:ratio='1' :ratio='1'
:src='imgPath' :src='state.imgPath'
) )
q-separator q-separator
q-card-actions q-card-actions
...@@ -118,67 +118,80 @@ q-card.icon-picker(flat, style='width: 400px;') ...@@ -118,67 +118,80 @@ q-card.icon-picker(flat, style='width: 400px;')
) )
</template> </template>
<script> <script setup>
import { find } from 'lodash-es' import { find } from 'lodash-es'
import { computed, onMounted, reactive } from 'vue'
export default { // PROPS
props: {
value: { const props = defineProps({
type: String, value: {
required: true type: String,
} required: true
}, }
data () { })
return {
currentTab: 'icon', // EMITS
selPack: 'las',
selIcon: '', const emit = defineEmits(['input'])
imgPath: 'https://placeimg.com/64/64/nature',
iconPacks: [ // DATA
{ 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' }, const state = reactive({
{ value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' }, currentTab: 'icon',
{ value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' }, selPack: 'las',
{ value: 'far', label: 'Font Awesome (regular)', name: 'Font Awesome', subset: 'regular', prefix: 'far fa-', reference: 'https://fontawesome.com/icons' }, selIcon: '',
{ value: 'fal', label: 'Font Awesome (light)', name: 'Font Awesome', subset: 'light', prefix: 'fal fa-', reference: 'https://fontawesome.com/icons' }, imgPath: 'https://placeimg.com/64/64/nature'
{ 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' }
] 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' },
computed: { { value: 'mdi', label: 'Material Design Icons', name: 'Material Design Icons', prefix: 'mdi-', reference: 'https://materialdesignicons.com' },
iconName () { { value: 'fas', label: 'Font Awesome (solid)', name: 'Font Awesome', subset: 'solid', prefix: 'fas fa-', reference: 'https://fontawesome.com/icons' },
return find(this.iconPacks, ['value', this.selPack]).prefix + this.selIcon { 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' },
iconPackRefWebsite () { { value: 'fad', label: 'Font Awesome (duotone)', name: 'Font Awesome', subset: 'duotone', prefix: 'fad fa-', reference: 'https://fontawesome.com/icons' },
return find(this.iconPacks, ['value', this.selPack]).reference { value: 'fab', label: 'Font Awesome (brands)', name: 'Font Awesome', subset: 'brands', prefix: 'fab fa-', reference: 'https://fontawesome.com/icons' }
} ]
},
mounted () { // COMPUTED
if (this.value?.startsWith('img:')) {
this.currentTab = 'img' const iconName = computed(() => {
this.imgPath = this.value.substring(4) return find(iconPacks, ['value', state.selPack]).prefix + state.selIcon
} else { })
this.currentTab = 'icon'
for (const pack of this.iconPacks) { const iconPackRefWebsite = computed(() => {
if (this.value?.startsWith(pack.prefix)) { return find(iconPacks, ['value', state.selPack]).reference
this.selPack = pack.value })
this.selIcon = this.value.substring(pack.prefix.length)
break // METHODS
}
} function apply () {
} if (state.currentTab === 'img') {
}, emit('input', `img:${state.imgPath}`)
methods: { } else {
apply () { emit('input', state.iconName)
if (this.currentTab === 'img') { }
this.$emit('input', `img:${this.imgPath}`) }
} else {
this.$emit('input', this.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> </script>
<style lang="scss"> <style lang="scss">
......
<template lang="pug"> <template lang="pug">
q-card.page-relation-dialog(style='width: 500px;') q-card.page-relation-dialog(style='width: 500px;')
q-toolbar.bg-primary.text-white q-toolbar.bg-primary.text-white
.text-subtitle2(v-if='isEditMode') {{$t('editor.pageRel.titleEdit')}} .text-subtitle2(v-if='isEditMode') {{t('editor.pageRel.titleEdit')}}
.text-subtitle2(v-else) {{$t('editor.pageRel.title')}} .text-subtitle2(v-else) {{t('editor.pageRel.title')}}
q-card-section q-card-section
.text-overline {{$t('editor.pageRel.position')}} .text-overline {{t('editor.pageRel.position')}}
q-form.q-gutter-md.q-pt-md q-form.q-gutter-md.q-pt-md
div div
q-btn-toggle( q-btn-toggle(
v-model='pos' v-model='state.pos'
push push
glossy glossy
no-caps no-caps
toggle-color='primary' toggle-color='primary'
:options=`[ :options=`[
{ label: $t('editor.pageRel.left'), value: 'left' }, { label: t('editor.pageRel.left'), value: 'left' },
{ label: $t('editor.pageRel.center'), value: 'center' }, { label: t('editor.pageRel.center'), value: 'center' },
{ label: $t('editor.pageRel.right'), value: 'right' } { label: t('editor.pageRel.right'), value: 'right' }
]` ]`
) )
.text-overline {{$t('editor.pageRel.button')}} .text-overline {{t('editor.pageRel.button')}}
q-input( q-input(
ref='iptRelLabel' ref='iptRelLabel'
outlined outlined
dense dense
:label='$t(`editor.pageRel.label`)' :label='t(`editor.pageRel.label`)'
v-model='label' v-model='state.label'
) )
template(v-if='pos !== `center`') template(v-if='state.pos !== `center`')
q-input( q-input(
outlined outlined
dense dense
:label='$t(`editor.pageRel.caption`)' :label='t(`editor.pageRel.caption`)'
v-model='caption' v-model='state.caption'
) )
q-btn.rounded-borders( q-btn.rounded-borders(
:label='$t(`editor.pageRel.selectIcon`)' :label='t(`editor.pageRel.selectIcon`)'
color='primary' color='primary'
outline outline
) )
q-menu(content-class='shadow-7') q-menu(content-class='shadow-7')
icon-picker-dialog(v-model='icon') icon-picker-dialog(v-model='state.icon')
.text-overline {{$t('editor.pageRel.target')}} .text-overline {{t('editor.pageRel.target')}}
q-btn.rounded-borders( q-btn.rounded-borders(
:label='$t(`editor.pageRel.selectPage`)' :label='t(`editor.pageRel.selectPage`)'
color='primary' color='primary'
outline outline
) )
.text-overline {{$t('editor.pageRel.preview')}} .text-overline {{t('editor.pageRel.preview')}}
q-btn( q-btn(
v-if='pos === `left`' v-if='state.pos === `left`'
padding='sm md' padding='sm md'
outline outline
:icon='icon' :icon='state.icon'
no-caps no-caps
color='primary' color='primary'
) )
.column.text-left.q-pl-md .column.text-left.q-pl-md
.text-body2: strong {{label}} .text-body2: strong {{state.label}}
.text-caption {{caption}} .text-caption {{state.caption}}
q-btn.full-width( q-btn.full-width(
v-else-if='pos === `center`' v-else-if='state.pos === `center`'
:label='label' :label='state.label'
color='primary' color='primary'
flat flat
no-caps no-caps
:icon='icon' :icon='state.icon'
) )
q-btn( q-btn(
v-else-if='pos === `right`' v-else-if='state.pos === `right`'
padding='sm md' padding='sm md'
outline outline
:icon-right='icon' :icon-right='state.icon'
no-caps no-caps
color='primary' color='primary'
) )
.column.text-left.q-pr-md .column.text-left.q-pr-md
.text-body2: strong {{label}} .text-body2: strong {{state.label}}
.text-caption {{caption}} .text-caption {{state.caption}}
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
icon='las la-times' icon='las la-times'
:label='$t(`common.actions.discard`)' :label='t(`common.actions.discard`)'
color='grey-7' color='grey-7'
padding='xs md' padding='xs md'
v-close-popup v-close-popup
...@@ -92,7 +92,7 @@ q-card.page-relation-dialog(style='width: 500px;') ...@@ -92,7 +92,7 @@ q-card.page-relation-dialog(style='width: 500px;')
v-if='isEditMode' v-if='isEditMode'
:disabled='!canSubmit' :disabled='!canSubmit'
icon='las la-check' icon='las la-check'
:label='$t(`common.actions.save`)' :label='t(`common.actions.save`)'
unelevated unelevated
color='primary' color='primary'
padding='xs md' padding='xs md'
...@@ -103,7 +103,7 @@ q-card.page-relation-dialog(style='width: 500px;') ...@@ -103,7 +103,7 @@ q-card.page-relation-dialog(style='width: 500px;')
v-else v-else
:disabled='!canSubmit' :disabled='!canSubmit'
icon='las la-plus' icon='las la-plus'
:label='$t(`common.actions.create`)' :label='t(`common.actions.create`)'
unelevated unelevated
color='primary' color='primary'
padding='xs md' padding='xs md'
...@@ -112,99 +112,127 @@ q-card.page-relation-dialog(style='width: 500px;') ...@@ -112,99 +112,127 @@ q-card.page-relation-dialog(style='width: 500px;')
) )
</template> </template>
<script> <script setup>
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { cloneDeep, find } from 'lodash-es' 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' import IconPickerDialog from './IconPickerDialog.vue'
export default { import { usePageStore } from 'src/stores/page'
components: { import { useSiteStore } from 'src/stores/site'
IconPickerDialog
}, // PROPS
props: {
editId: { const props = defineProps({
type: String, editId: {
default: null type: String,
} default: null
}, }
data () { })
return {
pos: 'left', // QUASAR
label: '',
caption: '', const $q = useQuasar()
icon: 'las la-arrow-left',
target: '' // STORES
}
}, const pageStore = usePageStore()
computed: { const siteStore = useSiteStore()
canSubmit () {
return this.label.length > 0 // I18N
},
isEditMode () { const { t } = useI18n()
return Boolean(this.editId)
// 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
} }
}, case 'center': {
watch: { state.icon = 'las la-book'
pos (newValue) { break
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 'right': {
mounted () { state.icon = 'las la-arrow-right'
if (this.editId) { break
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
}
} }
this.$nextTick(() => { }
this.$refs.iptRelLabel.focus() })
})
}, // METHODS
methods: {
create () { function create () {
this.$store.set('page/relations', [ pageStore.$patch({
...this.$store.get('page/relations'), relations: [
{ ...pageStore.relations,
id: uuid(), {
position: this.pos, id: uuid(),
label: this.label, position: state.pos,
...(this.pos !== 'center' ? { caption: this.caption } : {}), label: state.label,
icon: this.icon, ...(state.pos !== 'center' ? { caption: state.caption } : {}),
target: this.target icon: state.icon,
} target: state.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
}
} }
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> </script>
...@@ -222,7 +222,9 @@ body::-webkit-scrollbar-thumb { ...@@ -222,7 +222,9 @@ body::-webkit-scrollbar-thumb {
@import './animation.scss'; @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/lib/codemirror.css';
// @import '~codemirror/theme/elegant.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 ...@@ -18,13 +18,13 @@ q-page.column
:icon='brd.icon' :icon='brd.icon'
:label='brd.title' :label='brd.title'
:aria-label='brd.title' :aria-label='brd.title'
:to='getFullPath(brd)' :to='brd.path'
) )
.col-auto.flex.items-center.justify-end .col-auto.flex.items-center.justify-end
template(v-if='!pageStore.isPublished') template(v-if='!pageStore.isPublished')
.text-caption.text-accent: strong Unpublished .text-caption.text-accent: strong Unpublished
q-separator.q-mx-sm(vertical) 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-header.row
//- PAGE ICON //- PAGE ICON
.col-auto.q-pl-md.flex.items-center .col-auto.q-pl-md.flex.items-center
...@@ -90,7 +90,7 @@ q-page.column ...@@ -90,7 +90,7 @@ q-page.column
style='height: 100%;' style='height: 100%;'
) )
.q-pa-md .q-pa-md
div(v-html='pageStore.render') .page-contents(v-html='pageStore.render')
template(v-if='pageStore.relations && pageStore.relations.length > 0') template(v-if='pageStore.relations && pageStore.relations.length > 0')
q-separator.q-my-lg q-separator.q-my-lg
.row.align-center .row.align-center
...@@ -288,14 +288,14 @@ q-page.column ...@@ -288,14 +288,14 @@ q-page.column
transition-hide='jump-right' transition-hide='jump-right'
class='floating-sidepanel' class='floating-sidepanel'
) )
component(:is='state.sideDialogComponent') component(:is='sideDialogs[state.sideDialogComponent]')
q-dialog( q-dialog(
v-model='state.showGlobalDialog' v-model='state.showGlobalDialog'
transition-show='jump-up' transition-show='jump-up'
transition-hide='jump-down' transition-hide='jump-down'
) )
component(:is='state.globalDialogComponent') component(:is='globalDialogs[state.globalDialogComponent]')
</template> </template>
<script setup> <script setup>
...@@ -303,17 +303,23 @@ import { useMeta, useQuasar, setCssVar } from 'quasar' ...@@ -303,17 +303,23 @@ import { useMeta, useQuasar, setCssVar } from 'quasar'
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue' import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import { usePageStore } from 'src/stores/page' import { usePageStore } from 'src/stores/page'
import { useSiteStore } from '../stores/site' import { useSiteStore } from 'src/stores/site'
// COMPONENTS // COMPONENTS
import SocialSharingMenu from '../components/SocialSharingMenu.vue' import SocialSharingMenu from '../components/SocialSharingMenu.vue'
import PageDataDialog from '../components/PageDataDialog.vue'
import PageTags from '../components/PageTags.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 // QUASAR
...@@ -441,22 +447,36 @@ const editUrl = computed(() => { ...@@ -441,22 +447,36 @@ const editUrl = computed(() => {
pagePath += !pageStore.path ? 'home' : pageStore.path pagePath += !pageStore.path ? 'home' : pageStore.path
return `/_edit/${pagePath}` return `/_edit/${pagePath}`
}) })
const lastModified = computed(() => {
return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
})
// WATCHERS // 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(() => state.toc, refreshTocExpanded)
watch(() => pageStore.tocDepth, refreshTocExpanded) watch(() => pageStore.tocDepth, refreshTocExpanded)
// METHODS // METHODS
function getFullPath ({ locale, path }) {
if (siteStore.useLocales) {
return `/${locale}/${path}`
} else {
return `/${path}`
}
}
function togglePageProperties () { function togglePageProperties () {
state.sideDialogComponent = 'PagePropertiesDialog' state.sideDialogComponent = 'PagePropertiesDialog'
state.showSideDialog = true state.showSideDialog = true
......
import { defineStore } from 'pinia' 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', { export const usePageStore = defineStore('page', {
state: () => ({ state: () => ({
isLoading: true,
mode: 'view', mode: 'view',
editor: 'wysiwyg', editor: 'wysiwyg',
editorMode: 'edit', editorMode: 'edit',
id: 0, id: 0,
authorId: 0, authorId: 0,
authorName: 'Unknown', authorName: '',
createdAt: '', createdAt: '',
description: 'How to install Wiki.js on Ubuntu 18.04 / 20.04', description: '',
isPublished: true, isPublished: true,
showInTree: true, showInTree: true,
locale: 'en', locale: 'en',
path: '', path: '',
publishEndDate: '', publishEndDate: '',
publishStartDate: '', publishStartDate: '',
tags: ['cities', 'canada'], tags: [],
title: 'Ubuntu', title: '',
icon: 'lab la-empire', icon: 'las la-file-alt',
updatedAt: '', updatedAt: '',
relations: [], relations: [],
scriptJsLoad: '', scriptJsLoad: '',
...@@ -35,20 +40,20 @@ export const usePageStore = defineStore('page', { ...@@ -35,20 +40,20 @@ export const usePageStore = defineStore('page', {
max: 2 max: 2
}, },
breadcrumbs: [ breadcrumbs: [
{ // {
id: 1, // id: 1,
title: 'Installation', // title: 'Installation',
icon: 'las la-file-alt', // icon: 'las la-file-alt',
locale: 'en', // locale: 'en',
path: 'installation' // path: 'installation'
}, // },
{ // {
id: 2, // id: 2,
title: 'Ubuntu', // title: 'Ubuntu',
icon: 'lab la-ubuntu', // icon: 'lab la-ubuntu',
locale: 'en', // locale: 'en',
path: 'installation/ubuntu' // path: 'installation/ubuntu'
} // }
], ],
effectivePermissions: { effectivePermissions: {
comments: { comments: {
...@@ -75,11 +80,62 @@ export const usePageStore = defineStore('page', { ...@@ -75,11 +80,62 @@ export const usePageStore = defineStore('page', {
}, },
commentsCount: 0, commentsCount: 0,
content: '', 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: {}, getters: {},
actions: { 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 * PAGE - CREATE
*/ */
pageCreate ({ editor, locale, path }) { 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