allow to edit tags in admin panel, fix #170

This commit is contained in:
lesion 2022-12-13 15:41:39 +01:00
parent 854bd6538a
commit 4463c75536
No known key found for this signature in database
GPG key ID: 352918250B012177
9 changed files with 194 additions and 6 deletions

View file

@ -33,7 +33,7 @@ v-container
:prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:disabled="!collection.id"
placeholder='Tutte'
placeholder='All'
@input.native='searchTags'
@focus='searchTags'
:delimiters="[',', ';']"

View file

@ -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']),

115
components/admin/Tags.vue Normal file
View file

@ -0,0 +1,115 @@
<template lang='pug'>
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')}} -
strong.ml-2 {{tag.tag}}
v-card-subtitle {{$tc('admin.edit_tag_help', tag.count)}}
v-card-text
p {{newTag}}
v-form(v-model='valid' ref='form' lazy-validation)
v-combobox(v-model='newTag'
:prepend-icon="mdiTag"
hide-no-data
persistent-hint
:items="tags"
:return-object='false'
item-value='tag'
item-text='tag'
:label="$t('common.tags')")
template(v-slot:item="{ item, on, attrs }")
span "{{item.tag}}" <small>({{item.count}})</small>
v-card-actions
v-spacer
v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }}
v-btn(@click='saveTag' 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')
</template>
<script>
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag } from '@mdi/js'
import { mapState } from 'vuex'
import debounce from 'lodash/debounce'
import get from 'lodash/get'
export default {
data( {$store} ) {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag,
loading: false,
dialog: false,
valid: false,
tag: {},
newTag: '',
tags: [],
search: '',
headers: [
{ value: 'tag', text: this.$t('common.tag') },
{ value: 'count', text: 'N.' },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
}
},
async fetch() {
this.tags = await this.$axios.$get('/tags')
},
computed: {
...mapState(['settings']),
},
methods: {
editTag(item) {
this.tag.tag = item.tag
this.tag.count = item.count
this.dialog = true
},
async saveTag() {
if (!this.$refs.form.validate()) return
this.loading = true
this.$nextTick( async () => {
await this.$axios.$put('/tag', { tag: this.tag.tag, newTag: this.newTag })
await this.$fetch()
this.loading = false
this.dialog = false
})
},
async removeTag(tag) {
const ret = await this.$root.$confirm('admin.delete_tag_confirm', { tag: tag.tag, n: tag.count })
if (!ret) { return }
try {
await this.$axios.$delete(`/tag/${encodeURIComponent(tag.tag)}`)
await this.$fetch()
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
}
}
}
</script>

View file

@ -82,6 +82,7 @@
"url": "URL",
"place": "Place",
"tags": "Tags",
"tag": "Tag",
"theme": "Theme",
"reset": "Reset",
"import": "Import",
@ -213,6 +214,7 @@
"hide_resource": "Hide resource",
"delete_resource": "Delete resource",
"delete_resource_confirm": "Are you sure you want to delete this resource?",
"delete_tag_confirm": "Are you sure you want to remove the tag \"{tag}\"? The tag will be removed from {n} events.",
"block_user": "Block user",
"filter_instances": "Filter instances",
"filter_users": "Filter users",
@ -244,6 +246,8 @@
"footer_links": "Footer links",
"delete_footer_link_confirm": "Sure to remove this link?",
"edit_place": "Edit place",
"edit_tag": "Edit tag",
"edit_tag_help": "You can change the tag by replacing it with a new one or merging it with an existing one. The {n} associated events will also be changed.",
"new_announcement": "New announcement",
"show_smtp_setup": "Email settings",
"smtp_hostname": "SMTP Hostname",

View file

@ -240,6 +240,8 @@
"footer_links": "Collegamenti del piè di pagina",
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
"edit_place": "Modifica luogo",
"edit_tag": "Modifica tag",
"edit_tag_help": "Puoi cambiare il tag sostituendolo con uno nuovo o unendolo ad uno gia' esistente. Verranno modificati anche i {n} eventi associati",
"new_announcement": "Nuovo annuncio",
"show_smtp_setup": "Impostazioni email",
"widget": "Widget",

View file

@ -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'),

View file

@ -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,48 @@ 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 updateTag (req, res) {
const oldtag = await Tag.findByPk(req.body.tag)
const newtag = await Tag.findByPk(req.body.newTag)
// if the new tag does not exists, just rename the old one
if (!newtag) {
oldtag.tag = req.body.newTag
await oldtag.update({ tag: req.body.newTag })
} else {
// in case it exists:
// - search for events with old tag
const events = await oldtag.getEvents()
// - substitute it with the new one
await oldtag.removeEvents(events)
await newtag.addEvents(events)
}
res.sendStatus(200)
},
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)
}
}
}

View file

@ -162,15 +162,21 @@ if (config.status !== 'READY') {
api.get('/export/:type', cors, exportController.export)
api.get('/place/all', isAdmin, placeController.getAll)
// - PLACES
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)
// - TAGS
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)
api.put('/tag', isAdmin, tagController.updateTag)
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES
api.get('/instances', isAdmin, instanceController.getAll)

View file

@ -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)