diff --git a/components/admin/Places.vue b/components/admin/Places.vue
index 152fff48..6c8c26b7 100644
--- a/components/admin/Places.vue
+++ b/components/admin/Places.vue
@@ -89,7 +89,7 @@ export default {
}
},
async fetch() {
- this.places = await this.$axios.$get('/place/all')
+ this.places = await this.$axios.$get('/places')
},
computed: {
...mapState(['settings']),
diff --git a/components/admin/Tags.vue b/components/admin/Tags.vue
new file mode 100644
index 00000000..e9a62111
--- /dev/null
+++ b/components/admin/Tags.vue
@@ -0,0 +1,102 @@
+
+v-container
+ v-card-title {{ $t('common.tags') }}
+ v-spacer
+ v-text-field(v-model='search'
+ :append-icon='mdiMagnify' outlined rounded
+ :label="$t('common.search')"
+ single-line hide-details)
+
+ v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
+ v-card
+ v-card-title {{ $t('admin.edit_tag') }}
+ v-card-text
+ v-form(v-model='valid' ref='form' lazy-validation)
+ v-text-field(
+ :rules="[$validators.required('common.name')]"
+ :label="$t('common.tag')"
+ v-model='tag.tag'
+ :placeholder='$t("common.tag")')
+
+ v-card-actions
+ v-spacer
+ v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }}
+ v-btn(@click='savePlace' color='primary' outlined :loading='loading'
+ :disable='!valid || loading') {{ $t('common.save') }}
+
+ v-card-text
+ v-data-table(
+ :headers='headers'
+ :items='tags'
+ :hide-default-footer='tags.length < 5'
+ :header-props='{ sortIcon: mdiChevronDown }'
+ :footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
+ :search='search')
+ template(v-slot:item.map='{ item }')
+ span {{item.latitude && item.longitude && 'YEP' }}
+ template(v-slot:item.actions='{ item }')
+ v-btn(@click='editTag(item)' color='primary' icon)
+ v-icon(v-text='mdiPencil')
+ nuxt-link(:to='`/tag/${item.tag}`')
+ v-icon(v-text='mdiEye')
+ v-btn(@click='removeTag(item)' color='primary' icon)
+ v-icon(v-text='mdiDeleteForever')
+
+
+
diff --git a/locales/en.json b/locales/en.json
index ea95bbfd..44f288e2 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -82,6 +82,7 @@
"url": "URL",
"place": "Place",
"tags": "Tags",
+ "tag": "Tag",
"theme": "Theme",
"reset": "Reset",
"import": "Import",
diff --git a/pages/Admin.vue b/pages/Admin.vue
index 4597fa86..6ddac5bd 100644
--- a/pages/Admin.vue
+++ b/pages/Admin.vue
@@ -26,6 +26,11 @@ v-container.container.pa-0.pa-md-3
v-tab-item(value='places')
Places
+ //- TAGS
+ v-tab(href='#tags') {{$t('common.tags')}}
+ v-tab-item(value='tags')
+ Tags
+
//- GEOCODING / MAPS
v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}}
v-tab-item(value='geolocation')
@@ -77,6 +82,7 @@ export default {
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
+ Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'),
Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'),
[process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'),
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
diff --git a/server/api/controller/tag.js b/server/api/controller/tag.js
index 1f2f882b..e15679ac 100644
--- a/server/api/controller/tag.js
+++ b/server/api/controller/tag.js
@@ -1,6 +1,8 @@
const Tag = require('../models/tag')
const Event = require('../models/event')
const uniq = require('lodash/uniq')
+const log = require('../../log')
+
const { where, fn, col, Op } = require('sequelize')
const exportController = require('./export')
@@ -45,6 +47,20 @@ module.exports = {
}
},
+
+
+ async getAll (_req, res) {
+ const tags = await Tag.findAll({
+ order: [[fn('COUNT', col('tag.tag')), 'DESC']],
+ attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']],
+ include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
+ group: ['tag.tag'],
+ raw: true,
+ })
+ return res.json(tags)
+ },
+
+
/**
* search for tags by query string
* sorted by usage
@@ -60,9 +76,30 @@ module.exports = {
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'],
limit: 10,
- subQuery:false
+ subQuery: false
})
return res.json(tags.map(t => t.tag))
+ },
+
+ async updateTag (req, res) {
+ const tag = await Tag.findByPk(req.body.tag)
+ await tag.update(req.body)
+ res.json(place)
+ },
+
+
+ async remove (req, res) {
+ log.info('Remove tag', req.params.tag)
+ const tagName = req.params.tag
+ try {
+ const tag = await Tag.findByPk(tagName)
+ await tag.destroy()
+ res.sendStatus(200)
+ } catch (e) {
+ log.error('Tag removal failed:', e)
+ res.sendStatus(404)
+ }
}
+
}
\ No newline at end of file
diff --git a/server/api/index.js b/server/api/index.js
index 812fac98..1173559a 100644
--- a/server/api/index.js
+++ b/server/api/index.js
@@ -162,15 +162,18 @@ if (config.status !== 'READY') {
api.get('/export/:type', cors, exportController.export)
- api.get('/place/all', isAdmin, placeController.getAll)
+ api.get('/places', isAdmin, placeController.getAll)
api.get('/place/:placeName', cors, placeController.getEvents)
api.get('/place', cors, placeController.search)
api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim)
api.get('/placeOSM/Photon/:place_details', cors, placeController._photon)
api.put('/place', isAdmin, placeController.updatePlace)
+ api.get('/tags', isAdmin, tagController.getAll)
api.get('/tag', cors, tagController.search)
api.get('/tag/:tag', cors, tagController.getEvents)
+ api.delete('/tag/:tag', isAdmin, tagController.remove)
+
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES
api.get('/instances', isAdmin, instanceController.getAll)
diff --git a/tests/app.test.js b/tests/app.test.js
index 9ed543ff..8411c6c2 100644
--- a/tests/app.test.js
+++ b/tests/app.test.js
@@ -285,10 +285,10 @@ describe('Place', () => {
})
test('admin should get all places', async () => {
- await request(app).get('/api/place/all')
+ await request(app).get('/api/places')
.expect(403)
- const response = await request(app).get('/api/place/all')
+ const response = await request(app).get('/api/places')
.auth(token.access_token, { type: 'bearer' })
.expect(200)