feat: admin tags (#1452)

This commit is contained in:
Nicolas Giard 2020-02-09 00:49:32 -05:00 committed by GitHub
parent 90fbc62917
commit 5c20f585a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 311 additions and 2 deletions

View File

@ -29,9 +29,12 @@
v-list-item-action(style='min-width:auto;')
v-chip(x-small, :color='darkMode ? `grey darken-3-d4` : `grey lighten-5`')
.caption.grey--text {{ info.pagesTotal }}
v-list-item(to='/tags', v-if='hasPermission([`manage:system`])', disabled)
v-list-item-avatar(size='24'): v-icon(color='grey lighten-2') mdi-tag-multiple
v-list-item(to='/tags', v-if='hasPermission([`manage:system`])')
v-list-item-avatar(size='24'): v-icon mdi-tag-multiple
v-list-item-title {{ $t('admin:tags.title') }}
v-list-item-action(style='min-width:auto;')
v-chip(x-small, :color='darkMode ? `grey darken-3-d4` : `grey lighten-5`')
.caption.grey--text {{ info.tagsTotal }}
v-list-item(to='/theme', color='primary', v-if='hasPermission([`manage:system`, `manage:theme`])')
v-list-item-avatar(size='24'): v-icon mdi-palette-outline
v-list-item-title {{ $t('admin:theme.title') }}
@ -154,6 +157,7 @@ const router = new VueRouter({
{ path: '/pages', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages.vue') },
{ path: '/pages/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') },
{ path: '/pages/visualize', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-visualize.vue') },
{ path: '/tags', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-tags.vue') },
{ path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-theme.vue') },
{ path: '/groups', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups.vue') },
{ path: '/groups/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups-edit.vue') },

View File

@ -0,0 +1,247 @@
<template lang='pug'>
v-container(fluid, grid-list-lg)
v-layout(row wrap)
v-flex(xs12)
.admin-header
img.animated.fadeInUp(src='/svg/icon-tags.svg', alt='Tags', style='width: 80px;')
.admin-header-title
.headline.primary--text.animated.fadeInLeft {{$t('tags.title')}}
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('tags.subtitle')}}
v-spacer
v-btn.animated.fadeInDown(outlined, color='grey', @click='refresh', large)
v-icon mdi-refresh
v-container.pa-0.mt-3(fluid, grid-list-lg)
v-layout(row)
v-flex(style='flex: 0 0 350px;')
v-card.animated.fadeInUp
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', flat)
v-text-field(
v-model='filter'
:label='$t(`admin:tags.filter`)'
hide-details
single-line
solo
flat
dense
color='teal'
:background-color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-2`'
prepend-inner-icon='mdi-magnify'
)
v-divider
v-list.py-2(dense, nav)
v-list-item(v-if='tags.length < 1')
v-list-item-avatar(size='24'): v-icon(color='grey') mdi-compass-off
v-list-item-content
.caption.grey--text {{$t('tags.emptyList')}}
v-list-item(
v-for='tag of filteredTags'
:key='tag.id'
:class='(tag.id === current.id) ? "teal" : ""'
@click='selectTag(tag)'
)
v-list-item-avatar(size='24', tile): v-icon(size='18', :color='tag.id === current.id ? `white` : `teal`') mdi-tag
v-list-item-title(:class='tag.id === current.id ? `white--text` : ``') {{tag.tag}}
v-flex.animated.fadeInUp.wait-p2s
template(v-if='current.id')
v-card
v-toolbar(dense, color='teal', flat, dark)
.subtitle-1 {{$t('tags.edit')}}
v-spacer
v-btn.pl-4(
color='white'
dark
outlined
small
:href='`/t/` + current.tag'
)
span.text-none {{$t('admin:tags.viewLinkedPages')}}
v-icon(right) mdi-chevron-right
v-card-text
v-text-field(
outlined
:label='$t("tags.tag")'
prepend-icon='mdi-tag'
v-model='current.tag'
counter='255'
)
v-text-field(
outlined
:label='$t("tags.label")'
prepend-icon='mdi-format-title'
v-model='current.title'
hide-details
)
v-card-chin
i18next.caption.pl-3(path='admin:tags.date', tag='div')
strong(place='created') {{current.createdAt | moment('from')}}
strong(place='updated') {{current.updatedAt | moment('from')}}
v-spacer
v-dialog(v-model='deleteTagDialog', max-width='500')
template(v-slot:activator='{ on }')
v-btn(color='red', outlined, v-on='on')
v-icon(color='red') mdi-trash-can-outline
v-card
.dialog-header.is-red {{$t('admin:tags.deleteConfirm')}}
v-card-text.pa-4
i18next(tag='span', path='admin:tags.deleteConfirmText')
strong(place='tag') {{ current.tag }}
v-card-actions
v-spacer
v-btn(text, @click='deleteTagDialog = false') {{$t('common:actions.cancel')}}
v-btn(color='red', dark, @click='deleteTag(current)') {{$t('common:actions.delete')}}
v-btn.px-5.mr-2(color='success', depressed, dark, @click='saveTag(current)')
v-icon(left) mdi-content-save
span {{$t('common:actions.save')}}
v-card(v-else)
v-card-text.grey--text(v-if='tags.length > 0') {{$t('tags.noSelectionText')}}
v-card-text.grey--text(v-else) {{$t('tags.noItemsText')}}
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
export default {
data() {
return {
tags: [],
current: {},
filter: '',
deleteTagDialog: false
}
},
computed: {
filteredTags () {
if (this.filter.length > 0) {
return _.filter(this.tags, t => t.tag.indexOf(this.filter) >= 0 || t.title.indexOf(this.filter) >= 0)
} else {
return this.tags
}
}
},
methods: {
selectTag(tag) {
this.current = tag
},
async deleteTag(tag) {
this.$store.commit(`loadingStart`, 'admin-tags-delete')
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation ($id: Int!) {
pages {
deleteTag (id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
id: tag.id
}
})
if (_.get(resp, 'data.pages.deleteTag.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
message: this.$t('tags.deleteSuccess'),
style: 'success',
icon: 'check'
})
this.refresh()
} else {
throw new Error(_.get(resp, 'data.pages.deleteTag.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'admin-tags-delete')
},
async saveTag(tag) {
this.$store.commit(`loadingStart`, 'admin-tags-save')
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation ($id: Int!, $tag: String!, $title: String!) {
pages {
updateTag (id: $id, tag: $tag, title: $title) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
id: tag.id,
tag: tag.tag,
title: tag.title
}
})
if (_.get(resp, 'data.pages.updateTag.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
message: this.$t('tags.saveSuccess'),
style: 'success',
icon: 'check'
})
this.current.updatedAt = new Date()
} else {
throw new Error(_.get(resp, 'data.pages.updateTag.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'admin-tags-save')
},
async refresh() {
await this.$apollo.queries.tags.refetch()
this.current = {}
this.$store.commit('showNotification', {
message: this.$t('tags.refreshSuccess'),
style: 'success',
icon: 'cached'
})
}
},
apollo: {
tags: {
query: gql`
{
pages {
tags {
id
tag
title
createdAt
updatedAt
}
}
}
`,
fetchPolicy: 'network-only',
update: (data) => _.cloneDeep(data.pages.tags),
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-tags-refresh')
}
}
}
}
</script>
<style lang='scss' scoped>
.clickable {
cursor: pointer;
&:hover {
background-color: rgba(mc('blue', '500'), .25);
}
}
</style>

View File

@ -6,6 +6,7 @@ query {
groupsTotal
pagesTotal
usersTotal
tagsTotal
}
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#d7dfe2" d="M6,43V33h37c0.552,0,1,0.448,1,1v8c0,0.552-0.448,1-1,1H6z"/><path fill="#b0bec5" d="M33.162 33L29.228 33 12.289 43 16.223 43z"/><path fill="#8c9eff" d="M36,43V33h7c0.552,0,1,0.448,1,1v8c0,0.552-0.448,1-1,1H36z"/><path fill="#5c6bc0" d="M25 34H35V42H25z" transform="rotate(90 30 38)"/><path fill="#3f51b5" d="M26 37.228L33.162 33 29.228 33 26 34.906z"/><path fill="#303f9f" d="M15 34H25V42H15z" transform="rotate(90 20 38)"/><path fill="#283593" d="M24 36.086L16 40.809 16 43 16.223 43 24 38.409z"/><path fill="#d7dfe2" d="M41.861,24.381l-4.067-6.889c-0.281-0.476-0.894-0.633-1.37-0.353L6,35.1v3.284L8.725,43h3.564 l29.219-17.25C41.984,25.47,42.142,24.856,41.861,24.381z"/><path fill="#b0bec5" d="M28.875 21.596L25.325 23.691 14.892 41.464 18.441 39.368z"/><path fill="#81c784" d="M35.48,29.309l-5.084-8.611l6.028-3.559c0.476-0.281,1.089-0.123,1.37,0.353l4.067,6.889 c0.281,0.476,0.123,1.089-0.353,1.37L35.48,29.309z"/><path fill="#43a047" d="M22.772 24.054H32.772V32.054H22.772z" transform="rotate(59.443 27.773 28.055)"/><path fill="#2e7d32" d="M14.16 29.137H24.16V37.137H14.16z" transform="rotate(59.443 19.161 33.139)"/><path fill="#2e7d32" d="M14.16 29.137H24.16V37.137H14.16z" transform="rotate(59.443 19.161 33.139)"/><path fill="#d7dfe2" d="M31.506,11.188l-6.899-4.05c-0.476-0.28-1.089-0.12-1.369,0.356L6,36.858v3.421L10.636,43h3.354 l17.872-30.443C32.142,12.08,31.982,11.468,31.506,11.188z"/><path fill="#1b5e20" d="M21.787 29.719L17.155 37.609 18.258 39.477 18.441 39.368 22.95 31.688z"/><path fill="#388e3c" d="M28.74 21.826L28.674 21.714 25.325 23.691 22.945 27.746 24.108 29.716z"/><path fill="#ffd740" d="M28.318,18.593l-8.624-5.063l3.544-6.037c0.28-0.476,0.892-0.636,1.369-0.356l6.899,4.05 c0.476,0.28,0.636,0.892,0.356,1.369L28.318,18.593z"/><path fill="#ffc400" d="M15.969 17.236H25.969V25.236H15.969z" transform="rotate(30.416 20.968 21.235)"/><path fill="#ff9800" d="M10.906 25.86H20.906V33.86H10.906z" transform="rotate(30.416 15.905 29.858)"/><path fill="#d7dfe2" d="M16,43H6V6c0-0.552,0.448-1,1-1h8c0.552,0,1,0.448,1,1V43z"/><path fill="#ff8a80" d="M16,13H6V6c0-0.552,0.448-1,1-1h8c0.552,0,1,0.448,1,1V13z"/><path fill="#ff5252" d="M6 15H16V23H6z"/><path fill="#d50000" d="M6 25H16V33H6z"/><path fill="#455a64" d="M11 37A1 1 0 1 0 11 39A1 1 0 1 0 11 37Z"/><path fill="#90a4ae" d="M16 35.652L16 40.809 18.258 39.477z"/><path fill="#b0bec5" d="M16 34.553L16 39.576 18.193 35.841zM16 22.957L16 25.277 18 26.451 18 24.132z"/><path fill="#ffab00" d="M16 22.957L18 24.132 18 16.417 16 19.824z"/><path fill="#f57c00" d="M16 34.553L18 35.727 18 26.451 16 25.277z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="96px" height="96px"><linearGradient id="eNrllKi~A~aex2Cvsji~Ia" x1="40.012" x2="40.012" y1="19.127" y2="27.441" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#6dc7ff"/><stop offset="1" stop-color="#e6abff"/></linearGradient><path fill="url(#eNrllKi~A~aex2Cvsji~Ia)" d="M40.012 19.822A3.414 3.414 0 1 0 40.012 26.650000000000002A3.414 3.414 0 1 0 40.012 19.822Z"/><linearGradient id="eNrllKi~A~aex2Cvsji~Ib" x1="23.444" x2="23.444" y1="3.001" y2="60.671" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#eNrllKi~A~aex2Cvsji~Ib)" d="M29.351 22.98L16.537 30.377 17.538 32.11 30.351 24.712z"/><linearGradient id="eNrllKi~A~aex2Cvsji~Ic" x1="25.721" x2="25.721" y1="3.001" y2="60.671" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#eNrllKi~A~aex2Cvsji~Ic)" d="M31.627 26.922L18.814 34.32 19.814 36.053 32.627 28.655z"/><linearGradient id="eNrllKi~A~aex2Cvsji~Id" x1="27.996" x2="27.996" y1="3.001" y2="60.671" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#eNrllKi~A~aex2Cvsji~Id)" d="M21.089 38.263L22.09 39.995 34.903 32.597 33.903 30.865z"/><linearGradient id="eNrllKi~A~aex2Cvsji~Ie" x1="32" x2="32" y1="3.001" y2="60.671" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#eNrllKi~A~aex2Cvsji~Ie)" d="M56.648,13.631c-1.35-2.337-3.529-4.01-6.136-4.708c-2.607-0.699-5.33-0.34-7.667,1.01 c-0.867,0.501-1.636,1.116-2.299,1.815l-2.599-0.831c-1.482-0.474-3.113-0.301-4.468,0.48L9.597,25.185 c-1.564,0.904-2.102,2.911-1.199,4.476l0.638,1.105L7.64,31.572c-0.758,0.438-1.3,1.145-1.526,1.989 c-0.227,0.846-0.11,1.729,0.327,2.486l10.243,17.741c0.606,1.05,1.71,1.638,2.843,1.638c0.556,0,1.118-0.142,1.633-0.438 L45.041,41.2c1.352-0.781,2.318-2.104,2.663-3.702l0.515-3.65c0.706-0.734,1.21-1.652,1.43-2.664l0.58-2.667 c0.95-0.225,1.862-0.587,2.721-1.082C57.775,24.648,59.434,18.455,56.648,13.631z M45.737,37.145 c-0.214,0.977-0.832,1.823-1.696,2.322L20.159,53.256c-0.608,0.355-1.391,0.144-1.743-0.467L8.173,35.048 c-0.171-0.295-0.216-0.64-0.128-0.969c0.088-0.329,0.3-0.605,0.595-0.774l1.396-0.806l8.605,14.904 c0.438,0.758,1.144,1.3,1.989,1.526c0.282,0.076,0.568,0.113,0.853,0.113c0.568,0,1.129-0.149,1.633-0.44L45.982,35.4L45.737,37.145 z M47.693,30.759c-0.212,0.976-0.83,1.821-1.695,2.321L22.116,46.869c-0.297,0.171-0.641,0.215-0.968,0.128 c-0.329-0.088-0.605-0.299-0.774-0.595L10.13,28.661c-0.352-0.609-0.142-1.392,0.467-1.743L34.479,13.13 c0.546-0.315,1.161-0.477,1.779-0.477c0.363,0,0.728,0.056,1.079,0.168l1.921,0.614c-1.072,1.761-1.595,3.836-1.44,5.972 c-0.004,0.002-0.009,0.003-0.013,0.006c-1.022,0.589-1.752,1.541-2.058,2.68c-0.305,1.139-0.148,2.329,0.441,3.35 c0.816,1.414,2.303,2.206,3.83,2.206c0.748,0,1.507-0.189,2.2-0.59c1.022-0.59,1.751-1.541,2.057-2.681 c0.305-1.139,0.148-2.329-0.441-3.35c-0.59-1.022-1.541-1.751-2.68-2.057c-0.447-0.119-0.9-0.146-1.349-0.126 c-0.037-1.732,0.466-3.397,1.426-4.78l8.329,2.663c0.632,0.201,1.002,0.843,0.86,1.491L47.693,30.759z M40.008,20.82 c0.21,0,0.421,0.027,0.629,0.083c0.622,0.167,1.143,0.566,1.465,1.125c0.323,0.559,0.408,1.209,0.242,1.832 c-0.167,0.623-0.566,1.144-1.125,1.466c-1.151,0.666-2.633,0.269-3.298-0.884c-0.323-0.559-0.408-1.209-0.242-1.832 c0.167-0.622,0.567-1.143,1.126-1.465C39.177,20.931,39.59,20.82,40.008,20.82z M55.726,20.78c-0.561,2.092-1.901,3.84-3.777,4.922 c-0.397,0.23-0.811,0.417-1.233,0.574l1.66-7.63c0.361-1.662-0.588-3.306-2.207-3.822l-7.437-2.378 c0.346-0.286,0.713-0.552,1.112-0.782c1.875-1.084,4.061-1.369,6.15-0.811c2.092,0.561,3.839,1.901,4.921,3.777 C55.999,16.506,56.286,18.689,55.726,20.78z"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -289,6 +289,46 @@ module.exports = {
return graphHelper.generateError(err)
}
},
/**
* DELETE TAG
*/
async deleteTag (obj, args, context) {
try {
const tagToDel = await WIKI.models.tags.query().findById(args.id)
if (tagToDel) {
await tagToDel.$relatedQuery('pages').unrelate()
await WIKI.models.tags.query().deleteById(args.id)
} else {
throw new Error('This tag does not exist.')
}
return {
responseResult: graphHelper.generateSuccess('Tag has been deleted.')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* UPDATE TAG
*/
async updateTag (obj, args, context) {
try {
const affectedRows = await WIKI.models.tags.query()
.findById(args.id)
.patch({
tag: args.tag,
title: args.title
})
if (affectedRows < 1) {
throw new Error('This tag does not exist.')
}
return {
responseResult: graphHelper.generateSuccess('Tag has been updated successfully.')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* FLUSH PAGE CACHE
*/

View File

@ -372,6 +372,10 @@ module.exports = {
async usersTotal () {
const total = await WIKI.models.users.query().count('* as total').first()
return _.toSafeInteger(total.total)
},
async tagsTotal () {
const total = await WIKI.models.tags.query().count('* as total').first()
return _.toSafeInteger(total.total)
}
}
}

View File

@ -102,6 +102,16 @@ type PageMutation {
id: Int!
): DefaultResponse @auth(requires: ["delete:pages", "manage:system"])
deleteTag(
id: Int!
): DefaultResponse @auth(requires: ["manage:system"])
updateTag(
id: Int!
tag: String!
title: String!
): DefaultResponse @auth(requires: ["manage:system"])
flushCache: DefaultResponse @auth(requires: ["manage:system"])
migrateToLocale(

View File

@ -86,6 +86,7 @@ type SystemInfo {
sslProvider: String @auth(requires: ["manage:system"])
sslStatus: String @auth(requires: ["manage:system"])
sslSubscriberEmail: String @auth(requires: ["manage:system"])
tagsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"])
telemetry: Boolean @auth(requires: ["manage:system"])
telemetryClientId: String @auth(requires: ["manage:system"])
upgradeCapable: Boolean @auth(requires: ["manage:system"])