diff --git a/client/components/admin.vue b/client/components/admin.vue
index eed8483c..b31f935f 100644
--- a/client/components/admin.vue
+++ b/client/components/admin.vue
@@ -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') },
diff --git a/client/components/admin/admin-tags.vue b/client/components/admin/admin-tags.vue
new file mode 100644
index 00000000..77466ada
--- /dev/null
+++ b/client/components/admin/admin-tags.vue
@@ -0,0 +1,247 @@
+
+ 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')}}
+
+
+
+
+
diff --git a/client/graph/admin/dashboard/dashboard-query-stats.gql b/client/graph/admin/dashboard/dashboard-query-stats.gql
index 48be7ce8..75bf2096 100644
--- a/client/graph/admin/dashboard/dashboard-query-stats.gql
+++ b/client/graph/admin/dashboard/dashboard-query-stats.gql
@@ -6,6 +6,7 @@ query {
groupsTotal
pagesTotal
usersTotal
+ tagsTotal
}
}
}
diff --git a/client/static/svg/icon-color-palette.svg b/client/static/svg/icon-color-palette.svg
new file mode 100644
index 00000000..5b8fb451
--- /dev/null
+++ b/client/static/svg/icon-color-palette.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/static/svg/icon-tags.svg b/client/static/svg/icon-tags.svg
new file mode 100644
index 00000000..c003235a
--- /dev/null
+++ b/client/static/svg/icon-tags.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js
index 0a831fe6..851d4ec4 100644
--- a/server/graph/resolvers/page.js
+++ b/server/graph/resolvers/page.js
@@ -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
*/
diff --git a/server/graph/resolvers/system.js b/server/graph/resolvers/system.js
index cafff776..50298363 100644
--- a/server/graph/resolvers/system.js
+++ b/server/graph/resolvers/system.js
@@ -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)
}
}
}
diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql
index 57a2df87..3be9881e 100644
--- a/server/graph/schemas/page.graphql
+++ b/server/graph/schemas/page.graphql
@@ -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(
diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql
index 5b418340..9af61074 100644
--- a/server/graph/schemas/system.graphql
+++ b/server/graph/schemas/system.graphql
@@ -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"])