feat: tags UI (wip) + save tags from page

pull/1016/head
Nick 5 years ago
parent 8e80b7471d
commit 5a7fd2d73e

@ -163,9 +163,10 @@ Vue.component('not-found', () => import(/* webpackChunkName: "not-found" */ './c
Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
Vue.component('profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
Vue.component('register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('search-results', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue'))
Vue.component('tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue'))
Vue.component('unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue'))
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('welcome', () => import(/* webpackChunkName: "welcome" */ './components/welcome.vue'))
Vue.component('nav-footer', () => import(/* webpackChunkName: "theme-page" */ './themes/' + process.env.CURRENT_THEME + '/components/nav-footer.vue'))

@ -37,6 +37,15 @@
v-card.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, flat, dark)
.subtitle-1 {{provider.title}}
v-spacer
v-switch(
dark
color='blue lighten-5'
label='Active'
v-model='provider.isEnabled'
hide-details
inset
)
v-card-text
v-form
.analytic-provider-logo
@ -68,6 +77,7 @@
prepend-icon='mdi-settings-box'
:hint='cfg.value.hint ? cfg.value.hint : ""'
persistent-hint
inset
)
v-textarea(
v-else-if='cfg.value.type === "string" && cfg.value.multiline'

@ -63,10 +63,19 @@
)
v-flex(xs12, lg9)
v-card.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, flat, dark)
.subtitle-1 {{strategy.title}}
v-spacer
v-switch(
dark
color='blue lighten-5'
label='Active'
v-model='strategy.isEnabled'
hide-details
inset
:disabled='strategy.key === `local`'
)
v-card-text
v-form
.authlogo
@ -104,6 +113,7 @@
prepend-icon='mdi-settings-box'
:hint='cfg.value.hint ? cfg.value.hint : ""'
persistent-hint
inset
)
v-textarea(
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
@ -136,6 +146,7 @@
color='primary'
:hint='$t(`admin:auth.selfRegistrationHint`)'
persistent-hint
inset
)
v-switch.ml-3(
v-if='strategy.key === `local`'
@ -145,6 +156,7 @@
color='primary'
hint='Protects against spam robots and malicious registrations.'
persistent-hint
inset
)
v-combobox.ml-3.mt-3(
:label='$t(`admin:auth.domainsWhitelist`)'
@ -187,6 +199,7 @@
color='primary'
:hint='$t(`admin:auth.force2faHint`)'
persistent-hint
inset
)
v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s

@ -19,7 +19,7 @@
easing='easeOutQuint'
)
v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-1.dashboard-card.animated.fadeInUp.wait-p2s(dark)
v-card.green.lighten-1.dashboard-card.animated.fadeInUp.wait-p2s(dark)
v-card-text
v-icon.dashboard-icon mdi-account
.overline {{$t('admin:dashboard.users')}}
@ -30,7 +30,7 @@
easing='easeOutQuint'
)
v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-2.dashboard-card.animated.fadeInUp.wait-p4s(dark)
v-card.indigo.lighten-1.dashboard-card.animated.fadeInUp.wait-p4s(dark)
v-card-text
v-icon.dashboard-icon mdi-account-group
.overline {{$t('admin:dashboard.groups')}}

@ -23,6 +23,7 @@
persistent-hint
label='LDAP Debug'
v-model='flags.ldapdebug'
inset
)
v-divider.mt-3
v-switch.mt-3(
@ -31,6 +32,7 @@
persistent-hint
label='SQL Query Logging'
v-model='flags.sqllog'
inset
)
</template>

@ -98,6 +98,7 @@
v-chip(label, color='white', small).indigo--text coming soon
v-card-text
v-switch(
inset
label='Asset Image Optimization'
color='indigo'
v-model='config.featureTinyPNG'
@ -118,6 +119,7 @@
v-divider.mt-3
v-switch(
inset
label='Page Ratings'
color='indigo'
v-model='config.featurePageRatings'
@ -128,6 +130,7 @@
v-divider.mt-3
v-switch(
inset
label='Page Comments'
color='indigo'
v-model='config.featurePageComments'
@ -138,6 +141,7 @@
v-divider.mt-3
v-switch(
inset
label='Personal Wikis'
color='indigo'
v-model='config.featurePersonalWikis'
@ -152,6 +156,7 @@
v-card-text
v-alert(outlined, color='red darken-2', icon='mdi-information-outline').body-2 Make sure to understand the implications before turning on / off a security feature.
v-switch.mt-3(
inset
label='Block IFrame Embedding'
color='red darken-2'
v-model='config.securityIframe'
@ -160,6 +165,7 @@
)
v-divider.mt-3
v-switch(
inset
label='Same Origin Referrer Policy'
color='red darken-2'
v-model='config.securityReferrerPolicy'
@ -169,6 +175,7 @@
v-divider.mt-3
v-switch(
inset
label='Enforce HSTS'
color='red darken-2'
v-model='config.securityHSTS'
@ -191,6 +198,7 @@
v-divider.mt-3
v-switch(
inset
label='Enforce CSP'
color='red darken-2'
v-model='config.securityCSP'

@ -40,6 +40,7 @@
v-list-item-subtitle(v-html='data.item.nativeName')
v-divider.mt-3
v-switch(
inset
v-model='autoUpdate'
:label='$t("admin:locale.autoUpdate.label")'
color='primary'
@ -52,6 +53,7 @@
v-toolbar-title.subtitle-1 {{ $t('admin:locale.namespacing') }}
v-card-text
v-switch(
inset
v-model='namespacing'
:label='$t("admin:locale.namespaces.label")'
color='primary'

@ -64,6 +64,7 @@
persistent-hint
:hint='$t(`admin:mail.smtpTLSHint`)'
prepend-icon='mdi-security-network'
inset
)
v-text-field.mt-3(
outlined
@ -94,6 +95,7 @@
:label='$t(`admin:mail.dkimUse`)'
color='primary'
prepend-icon='mdi-key'
inset
)
v-text-field(
outlined

@ -82,6 +82,7 @@
label='Enabled'
v-model='currentRenderer.isEnabled'
hide-details
inset
)
v-card-text.pb-4.pt-2.pl-4
.overline.my-5 Rendering Module Configuration
@ -106,6 +107,7 @@
color='primary'
:hint='cfg.value.hint ? cfg.value.hint : ""'
persistent-hint
inset
)
v-text-field(
v-else

@ -69,6 +69,7 @@
prepend-icon='mdi-settings-box'
:hint='cfg.value.hint ? cfg.value.hint : ""'
persistent-hint
inset
)
v-textarea(
v-else-if='cfg.value.type === "string" && cfg.value.multiline'

@ -80,6 +80,15 @@
v-card.wiki-form.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, flat, dark)
.subtitle-1 {{target.title}}
v-spacer
v-switch(
dark
color='blue lighten-5'
label='Active'
v-model='target.isEnabled'
hide-details
inset
)
v-card-text
v-form
.targetlogo
@ -115,6 +124,7 @@
prepend-icon='mdi-settings-box'
:hint='cfg.value.hint ? cfg.value.hint : ""'
persistent-hint
inset
)
v-textarea(
v-else-if='cfg.value.type === "string" && cfg.value.multiline'

@ -44,6 +44,7 @@
)
v-divider.mt-3
v-switch(
inset
v-model='darkMode'
:label='$t(`admin:theme.darkMode`)'
color='primary'

@ -125,6 +125,11 @@
//- v-btn(depressed, color='grey darken-3', block)
//- v-icon(left) mdi-cached
//- span Reset
v-tooltip(bottom, v-if='isAuthenticated && isAdmin')
template(v-slot:activator='{ on }')
v-btn.ml-2.mr-0(icon, v-on='on', href='/t')
v-icon(color='grey') mdi-tag-multiple
span Browse Tags
v-flex(xs6, md4)
v-toolbar.nav-header-inner.pr-4(color='black', dark, flat)
v-spacer

@ -90,6 +90,7 @@
:hint='$t(`editor:props.publishToggleHint`)'
persistent-hint
disabled
inset
)
v-divider
v-card-text.grey.pt-5(:class='darkMode ? `darken-3-d3` : `lighten-5`')
@ -190,6 +191,7 @@
:hint='$t(`editor:props.allowCommentsHint`)'
persistent-hint
disabled
inset
)
v-switch(
:label='$t(`editor:props.allowRatings`)'
@ -198,6 +200,7 @@
:hint='$t(`editor:props.allowRatingsHint`)'
persistent-hint
disabled
inset
)
v-switch(
:label='$t(`editor:props.displayAuthor`)'
@ -206,6 +209,7 @@
:hint='$t(`editor:props.displayAuthorHint`)'
persistent-hint
disabled
inset
)
v-switch(
:label='$t(`editor:props.displaySharingBar`)'
@ -214,6 +218,7 @@
:hint='$t(`editor:props.displaySharingBarHint`)'
persistent-hint
disabled
inset
)
page-selector(mode='create', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')

@ -0,0 +1,167 @@
<template lang='pug'>
v-app(:dark='darkMode').tags
nav-header
v-navigation-drawer.pb-0.elevation-1(app, fixed, clipped, :right='$vuetify.rtl', permanent, width='300')
vue-scroll(:ops='scrollStyle')
v-list(dense, nav)
v-list-item(href='/')
v-list-item-icon: v-icon mdi-home
v-list-item-title {{$t('common:header.home')}}
template(v-for='(tags, groupName) in tagsGrouped')
v-divider.my-2
v-subheader.pl-4(:key='`tagGroup-` + groupName') {{groupName}}
v-list-item(v-for='tag of tags', @click='toggleTag(tag.tag)', :key='`tag-` + tag.tag')
v-list-item-icon
v-icon(v-if='isSelected(tag.tag)', color='primary') mdi-checkbox-intermediate
v-icon(v-else) mdi-checkbox-blank-outline
v-list-item-title {{tag.title}}
v-content
v-toolbar(color='primary', dark, flat, height='58')
template(v-if='selection.length > 0')
.overline.mr-3.animated.fadeInLeft Current Selection
v-chip.mr-3.primary--text(
v-for='tag of tagsSelected'
color='white'
close
@click:close='toggleTag(tag.tag)'
) {{tag.title}}
v-spacer
v-btn.animated.fadeIn(
small
outlined
color='blue lighten-4'
rounded
@click='selection = []'
)
v-icon(left) mdi-close
span Clear Selection
template(v-else)
v-icon.mr-3.animated.fadeInRight mdi-arrow-left
.overline.animated.fadeInRight Select one or more tags
v-toolbar(color='grey lighten-4', flat, height='58')
v-text-field.tags-search(
label='Search within results...'
solo
hide-details
flat
rounded
single-line
height='40'
prepend-icon='mdi-file-document-box-search-outline'
append-icon='mdi-arrow-right'
)
v-divider.mx-3(vertical)
.overline Order By
v-select.ml-2(
:items='orderByItems'
v-model='orderBy'
background-color='white'
hide-details
label='Order By'
rounded
single-line
dense
height='40'
style='max-width: 250px;'
)
v-divider.mx-3(vertical)
v-btn-toggle(v-model='displayStyle', rounded, mandatory)
v-btn(text, height='40'): v-icon(small) mdi-view-list
v-btn(text, height='40'): v-icon(small) mdi-cards-variant
v-btn(text, height='40'): v-icon(small) mdi-format-align-justify
v-divider
nav-footer
notify
search-results
</template>
<script>
import { get } from 'vuex-pathify'
import _ from 'lodash'
import tagsQuery from 'gql/common/common-pages-query-tags.gql'
export default {
data() {
return {
tags: [],
selection: [],
displayStyle: 0,
orderBy: 'TITLE',
orderByItems: [
{ text: 'Creation Date', value: 'CREATED' },
{ text: 'ID', value: 'ID' },
{ text: 'Last Modified', value: 'UPDATED' },
{ text: 'Path', value: 'PATH' },
{ text: 'Title', value: 'TITLE' }
],
scrollStyle: {
vuescroll: {},
scrollPanel: {
initialScrollY: 0,
initialScrollX: 0,
scrollingX: false,
easing: 'easeOutQuad',
speed: 1000,
verticalNativeBarPos: this.$vuetify.rtl ? `left` : `right`
},
rail: {
gutterOfEnds: '2px'
},
bar: {
onlyShowBarOnScroll: false,
background: '#CCC',
hoverStyle: {
background: '#999'
}
}
}
}
},
computed: {
darkMode: get('site/dark'),
tagsGrouped () {
return _.groupBy(this.tags, t => t.title.charAt(0).toUpperCase())
},
tagsSelected () {
return _.filter(this.tags, t => _.includes(this.selection, t.tag))
}
},
created () {
this.$store.commit('page/SET_MODE', 'tags')
},
methods: {
toggleTag (tag) {
if (_.includes(this.selection, tag)) {
this.selection = _.without(this.selection, tag)
} else {
this.selection.push(tag)
}
},
isSelected (tag) {
return _.includes(this.selection, tag)
}
},
apollo: {
tags: {
query: tagsQuery,
fetchPolicy: 'cache-and-network',
update: (data) => _.cloneDeep(data.pages.tags),
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'tags-refresh')
}
}
}
}
</script>
<style lang='scss'>
.tags-search {
.v-input__control {
min-height: initial !important;
}
.v-input__prepend-outer {
margin-top: 8px !important;
}
}
</style>

@ -0,0 +1,8 @@
query {
pages {
tags {
tag
title
}
}
}

@ -165,6 +165,14 @@ router.get(['/s', '/s/*'], async (req, res, next) => {
}
})
/**
* Tags
*/
router.get(['/t', '/t/*'], (req, res, next) => {
_.set(res.locals, 'pageMeta.title', 'Tags')
res.render('tags')
})
/**
* View document / asset
*/

@ -76,6 +76,9 @@ module.exports = {
} else {
throw new WIKI.Error.PageNotFound()
}
},
async tags (obj, args, context, info) {
return WIKI.models.tags.query().orderBy('tag', 'asc')
}
},
PageMutation: {

@ -36,6 +36,8 @@ type PageQuery {
single(
id: Int!
): Page @auth(requires: ["manage:pages", "delete:pages", "manage:system"])
tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
}
# -----------------------------------------------
@ -109,6 +111,7 @@ type Page {
privateNS: String
publishStartDate: Date!
publishEndDate: String!
tags: [PageTag]!
content: String!
render: String
toc: String
@ -125,6 +128,14 @@ type Page {
creatorEmail: String!
}
type PageTag {
id: Int!
tag: String!
title: String
createdAt: Date!
updatedAt: Date!
}
type PageHistory {
versionId: Int!
authorId: Int!

@ -210,6 +210,11 @@ module.exports = class Page extends Model {
isPrivate: opts.isPrivate
})
// -> Save Tags
if (opts.tags.length > 0) {
await WIKI.models.tags.associateTags({ tags: opts.tags, page })
}
// -> Render page to HTML
await WIKI.models.pages.renderPage(page)
@ -260,6 +265,9 @@ module.exports = class Page extends Model {
isPrivate: ogPage.isPrivate
})
// -> Save Tags
await WIKI.models.tags.associateTags({ tags: opts.tags, page })
// -> Render page to HTML
await WIKI.models.pages.renderPage(page)

@ -1,4 +1,7 @@
const Model = require('objection').Model
const _ = require('lodash')
/* global WIKI */
/**
* Tags model
@ -46,4 +49,51 @@ module.exports = class Tag extends Model {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
static async associateTags ({ tags, page }) {
let existingTags = await WIKI.models.tags.query().column('id', 'tag')
// Create missing tags
const newTags = _.filter(tags, t => !_.some(existingTags, ['tag', t])).map(t => ({
tag: t,
title: t
}))
if (newTags.length > 0) {
if (WIKI.config.db.type === 'postgres') {
const createdTags = await WIKI.models.tags.query().insert(newTags)
existingTags = _.concat(existingTags, createdTags)
} else {
for (const newTag of newTags) {
const createdTag = await WIKI.models.tags.query().insert(newTag)
existingTags.push(createdTag)
}
}
}
// Fetch current page tags
const targetTags = _.filter(existingTags, t => _.includes(tags, t.tag))
const currentTags = await page.$relatedQuery('tags')
// Tags to relate
const tagsToRelate = _.differenceBy(targetTags, currentTags, 'id')
if (tagsToRelate.length > 0) {
if (WIKI.config.db.type === 'postgres') {
await page.$relatedQuery('tags').relate(tagsToRelate)
} else {
for (const tag of tagsToRelate) {
await page.$relatedQuery('tags').relate(tag)
}
}
}
// Tags to unrelate
const tagsToUnrelate = _.differenceBy(currentTags, targetTags, 'id')
if (tagsToUnrelate.length > 0) {
await page.$relatedQuery('tags').unrelate().whereIn('tags.id', _.map(tagsToUnrelate, 'id'))
}
}
}

@ -0,0 +1,5 @@
extends master.pug
block body
#root
tags
Loading…
Cancel
Save