refactoring search, filtering, selection, fix #225 #227 #224

This commit is contained in:
lesion 2023-01-09 17:02:15 +01:00
parent 901c11e6cc
commit 0891acce1d
No known key found for this signature in database
GPG key ID: 352918250B012177
9 changed files with 187 additions and 102 deletions

View file

@ -1,20 +1,51 @@
<template lang="pug"> <template lang="pug">
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar') #navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar || showCalendar')
v-text-field.mx-2(v-if='showSearchBar' outlined dense hide-details :placeholder='$t("common.search")' :append-icon='mdiMagnify' @input='search' clearable :clear-icon='mdiClose')
template(v-slot:prepend-inner) div.mx-2
Calendar(v-if='!settings.hide_calendar') client-only(v-if='showSearchBar')
v-btn.ml-2.mt-2.gap-2(v-if='showCollectionsBar' small outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}} v-menu(offset-y :close-on-content-click='false' tile)
template(v-slot:activator="{on ,attrs}")
v-text-field(hide-details outlined
:placeholder='$t("common.search")'
@input="v => setFilter(['query', v])" clearable :clear-icon='mdiClose')
template(v-slot:append)
v-icon(v-text='mdiCog' v-bind='attrs' v-on='on')
v-card(outlined :rounded='"0"')
v-card-text
v-row(dense)
v-col(v-if='settings.allow_recurrent_event')
v-switch.mt-0(v-model='show_recurrent' @change="v => setFilter(['show_recurrent', v])"
hide-details :label="$t('event.show_recurrent')" inset)
v-col(v-if='settings.allow_multidate_event')
v-switch.mt-0(v-model='show_multidate' @change="v => setFilter(['show_multidate', v])"
hide-details :label="$t('event.show_multidate')" inset)
v-row(v-if='!showCalendar')
v-col
Calendar.mt-2
v-text-field(slot='placeholder' outlined hide-details :placeholder="$t('common.search')" :append-icon='mdiCog')
span(v-if='showCollectionsBar')
v-btn.mr-2.mt-2(small outlined v-for='collection in collections'
color='primary' :key='collection.id'
:to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
Calendar.mt-2(v-if='showCalendar')
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState, mapActions } from 'vuex'
import Calendar from '@/components/Calendar' import Calendar from '@/components/Calendar'
import { mdiMagnify, mdiClose } from '@mdi/js' import { mdiClose, mdiCog } from '@mdi/js'
export default { export default {
data: () => ({ data: ({ $store }) => ({
mdiMagnify, mdiClose,
oldRoute: '', oldRoute: '',
collections: [] mdiClose, mdiCog,
collections: [],
show_recurrent: $store.state.settings.recurrent_event_visible,
show_multidate: true,
query: ''
}), }),
async fetch () { async fetch () {
this.collections = await this.$axios.$get('collections').catch(_e => []) this.collections = await this.$axios.$get('collections').catch(_e => [])
@ -24,6 +55,9 @@ export default {
showSearchBar () { showSearchBar () {
return this.$route.name === 'index' return this.$route.name === 'index'
}, },
showCalendar () {
return (!this.settings.hide_calendar && this.$route.name === 'index')
},
showCollectionsBar () { showCollectionsBar () {
const show = ['index', 'collection-collection'].includes(this.$route.name) const show = ['index', 'collection-collection'].includes(this.$route.name)
if (show && this.oldRoute !== this.$route.name) { if (show && this.oldRoute !== this.$route.name) {
@ -32,18 +66,16 @@ export default {
} }
return show return show
}, },
...mapState(['settings']) ...mapState(['settings', 'filter'])
}, },
methods: { methods: {
search (ev) { ...mapActions(['setFilter']),
this.$root.$emit('search', ev)
}
} }
} }
</script> </script>
<style> <style>
#navsearch { #navsearch {
margin: 0 auto; margin: 0 auto;
max-width: 800px; max-width: 700px;
} }
</style> </style>

View file

@ -125,9 +125,14 @@ export default {
return matches return matches
} }
}, },
mounted () {
this.$nextTick( () => {
this.search()
})
},
methods: { methods: {
search: debounce(async function(ev) { search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase() const search = ev ? ev.target.value.trim().toLowerCase() : ''
this.places = await this.$axios.$get(`place?search=${search}`) this.places = await this.$axios.$get(`place?search=${search}`)
if (!search && this.places.length) { return this.places } if (!search && this.places.length) { return this.places }
const matches = this.places.find(p => search === p.name.toLocaleLowerCase()) const matches = this.places.find(p => search === p.name.toLocaleLowerCase())

View file

@ -147,6 +147,7 @@
"recurrent": "Recurring", "recurrent": "Recurring",
"edit_recurrent": "Edit recurring event:", "edit_recurrent": "Edit recurring event:",
"show_recurrent": "recurring events", "show_recurrent": "recurring events",
"show_multidate": "multidate events",
"show_past": "also prior events", "show_past": "also prior events",
"only_future": "only upcoming events", "only_future": "only upcoming events",
"recurrent_description": "Choose frequency and select days", "recurrent_description": "Choose frequency and select days",

View file

@ -179,7 +179,6 @@ export default {
filteredTags() { filteredTags() {
if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) } if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) }
const tagName = this.tagName.trim().toLowerCase() const tagName = this.tagName.trim().toLowerCase()
console.log(tagName)
return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag) return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag)
} }
}, },
@ -245,6 +244,8 @@ export default {
if (this.date.dueHour) { if (this.date.dueHour) {
[hour, minute] = this.date.dueHour.split(':') [hour, minute] = this.date.dueHour.split(':')
formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix()) formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix())
} else if (!!this.date.multidate) {
formData.append('end_datetime', dayjs(this.date.due).hour(24).minute(0).second(0).unix())
} }
if (this.edit) { if (this.edit) {

View file

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
v-container.px-2.px-sm-6.pt-0 v-container.px-2.px-sm-6.pt-0
//- Announcements //- Announcements
#announcements.mt-2.mt-sm-4(v-if='announcements.length') #announcements.mt-2.mt-sm-4(v-if='announcements.length')
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement') Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
@ -41,7 +40,8 @@ export default {
searching: false, searching: false,
tmpEvents: [], tmpEvents: [],
selectedDay: null, selectedDay: null,
show_recurrent: $store.state.settings.recurrent_event_visible, storeUnsubscribe: null
} }
}, },
head () { head () {
@ -63,53 +63,61 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['settings', 'announcements', 'events']), ...mapState(['settings', 'announcements', 'events', 'filter']),
visibleEvents () { visibleEvents () {
if (this.searching) { if (this.filter.query) {
return this.tmpEvents return this.tmpEvents
} }
const now = dayjs().unix() const now = dayjs().unix()
if (this.selectedDay) { if (this.selectedDay) {
const min = dayjs.tz(this.selectedDay).startOf('day').unix() const min = dayjs.tz(this.selectedDay).startOf('day').unix()
const max = dayjs.tz(this.selectedDay).endOf('day').unix() const max = dayjs.tz(this.selectedDay).endOf('day').unix()
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.show_recurrent || !e.parentId)) return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.filter.show_recurrent || !e.parentId))
} else if (this.isCurrentMonth) { } else if (this.isCurrentMonth) {
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.show_recurrent || !e.parentId))) return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.filter.show_recurrent || !e.parentId)))
} else { } else {
return this.events.filter(e => this.show_recurrent || !e.parentId) return this.events.filter(e => this.filter.show_recurrent || !e.parentId)
} }
} }
}, },
created () { mounted () {
this.$root.$on('dayclick', this.dayChange) this.$root.$on('dayclick', this.dayChange)
this.$root.$on('monthchange', this.monthChange) this.$root.$on('monthchange', this.monthChange)
this.$root.$on('search', debounce(this.search, 100)) this.storeUnsubscribe = this.$store.subscribeAction( { after: (action, state) => {
if (action.type === 'setFilter') {
if (this.filter.query) {
this.search()
} else {
this.updateEvents()
}
}
}})
console.error(this.storeUnsubscribe)
}, },
destroyed () { destroyed () {
this.$root.$off('dayclick') this.$root.$off('dayclick')
this.$root.$off('monthchange') this.$root.$off('monthchange')
this.$root.$off('search') if (typeof this.storeUnsubscribe === 'function') {
this.storeUnsubscribe()
}
}, },
methods: { methods: {
...mapActions(['getEvents']), ...mapActions(['getEvents']),
async search (query) { search: debounce(async function() {
if (query) { this.tmpEvents = await this.$api.getEvents({
this.tmpEvents = await this.$axios.$get(`/event/search?search=${query}`) start: 0,
this.searching = true show_recurrent: this.filter.show_recurrent,
} else { show_multidate: this.filter.show_multidate,
this.tmpEvents = null query: this.filter.query
this.searching = false })
} }, 100),
},
updateEvents () { updateEvents () {
return this.getEvents({ return this.getEvents({
start: this.start, start: this.start,
end: this.end, end: this.end
show_recurrent: true
}) })
}, },
async monthChange ({ year, month }) { async monthChange ({ year, month }) {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
this.$nextTick( async () => { this.$nextTick( async () => {

View file

@ -18,12 +18,15 @@ export default ({ $axios }, inject) => {
try { try {
const events = await $axios.$get('/events', { const events = await $axios.$get('/events', {
params: { params: {
start: params.start, ...params,
end: params.end, // start: params.start,
// end: params.end,
places: params.places && params.places.join(','), places: params.places && params.places.join(','),
tags: params.tags && params.tags.join(','), tags: params.tags && params.tags.join(','),
show_recurrent: !!params.show_recurrent, // ...(params.show_recurrent !== && {show_recurrent: !!params.show_recurrent}),
max: params.maxs // show_multidate: !!params.show_multidate,
// query: params.query,
// max: params.maxs
} }
}) })
return events.map(e => Object.freeze(e)) return events.map(e => Object.freeze(e))

View file

@ -3,7 +3,6 @@ const path = require('path')
const config = require('../../config') const config = require('../../config')
const fs = require('fs') const fs = require('fs')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const intersection = require('lodash/intersection')
const linkifyHtml = require('linkify-html') const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const dayjs = require('dayjs') const dayjs = require('dayjs')
@ -87,71 +86,71 @@ const eventController = {
}, },
async search(req, res) { // async search(req, res) {
const search = req.query.search.trim().toLocaleLowerCase() // const search = req.query.search.trim().toLocaleLowerCase()
const show_recurrent = req.query.show_recurrent || false // const show_recurrent = req.query.show_recurrent || false
const end = req.query.end // const end = req.query.end
const replacements = [] // const replacements = []
const where = { // const where = {
// do not include parent recurrent event // // do not include parent recurrent event
recurrent: null, // recurrent: null,
// confirmed event only // // confirmed event only
is_visible: true, // is_visible: true,
} // }
if (!show_recurrent) { // if (!show_recurrent) {
where.parentId = null // where.parentId = null
} // }
if (end) { // if (end) {
where.start_datetime = { [Op.lte]: end } // where.start_datetime = { [Op.lte]: end }
} // }
if (search) { // if (search) {
replacements.push(search) // replacements.push(search)
where[Op.or] = // where[Op.or] =
[ // [
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') }, // { title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'), // Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`)) // Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
] // ]
} // }
const events = await Event.findAll({ // const events = await Event.findAll({
where, // where,
attributes: { // attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources'] // exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
}, // },
order: [['start_datetime', 'DESC']], // order: [['start_datetime', 'DESC']],
include: [ // include: [
{ // {
model: Tag, // model: Tag,
// order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')], // // order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'], // attributes: ['tag'],
through: { attributes: [] } // through: { attributes: [] }
}, // },
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] } // { model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
], // ],
replacements, // replacements,
limit: 30, // limit: 30,
}).catch(e => { // }).catch(e => {
log.error('[EVENT]', e) // log.error('[EVENT]', e)
return res.json([]) // return res.json([])
}) // })
const ret = events.map(e => { // const ret = events.map(e => {
e = e.get() // e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : [] // e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e // return e
}) // })
return res.json(ret) // return res.json(ret)
}, // },
async _get(slug) { async _get(slug) {
// retrocompatibility, old events URL does not use slug, use id as fallback // retrocompatibility, old events URL does not use slug, use id as fallback
@ -600,9 +599,11 @@ const eventController = {
async _select({ async _select({
start = dayjs().unix(), start = dayjs().unix(),
end, end,
query,
tags, tags,
places, places,
show_recurrent, show_recurrent,
show_multidate,
limit, limit,
page, page,
older }) { older }) {
@ -625,6 +626,10 @@ const eventController = {
where.parentId = null where.parentId = null
} }
if (!show_multidate) {
where.multidate = { [Op.not]: true }
}
if (end) { if (end) {
where.start_datetime = { [older ? Op.gte : Op.lte]: end } where.start_datetime = { [older ? Op.gte : Op.lte]: end }
} }
@ -648,6 +653,16 @@ const eventController = {
where.placeId = places.split(',') where.placeId = places.split(',')
} }
if (query) {
replacements.push(query)
where[Op.or] =
[
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + query + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + query + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
]
}
let pagination = {} let pagination = {}
if (limit) { if (limit) {
pagination = { pagination = {
@ -692,17 +707,21 @@ const eventController = {
const settings = res.locals.settings const settings = res.locals.settings
const start = req.query.start || dayjs().unix() const start = req.query.start || dayjs().unix()
const end = req.query.end const end = req.query.end
const query = req.query.query
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const limit = Number(req.query.max) || 0 const limit = Number(req.query.max) || 0
const page = Number(req.query.page) || 0 const page = Number(req.query.page) || 0
const older = req.query.older || false const older = req.query.older || false
const show_multidate = settings.allow_multidate_event &&
typeof req.query.show_multidate !== 'undefined' ? req.query.show_multidate !== 'false' : true
const show_recurrent = settings.allow_recurrent_event && const show_recurrent = settings.allow_recurrent_event &&
typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible
res.json(await eventController._select({ res.json(await eventController._select({
start, end, places, tags, show_recurrent, limit, page, older start, end, query, places, tags, show_recurrent, show_multidate, limit, page, older
})) }))
}, },

View file

@ -91,6 +91,7 @@ module.exports = () => {
* @type GET * @type GET
* @param {integer} [start] - start timestamp (default: now) * @param {integer} [start] - start timestamp (default: now)
* @param {integer} [end] - end timestamp (optional) * @param {integer} [end] - end timestamp (optional)
* @param {string} [query] - search for this string
* @param {array} [tags] - List of tags * @param {array} [tags] - List of tags
* @param {array} [places] - List of places id * @param {array} [places] - List of places id
* @param {integer} [max] - Limit events * @param {integer} [max] - Limit events
@ -128,7 +129,7 @@ module.exports = () => {
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection) // allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add)
api.get('/event/search', eventController.search) // api.get('/event/search', eventController.search)
api.put('/event', isAuth, upload.single('image'), eventController.update) api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL) api.get('/event/import', isAuth, helpers.importURL)

View file

@ -23,6 +23,11 @@ export const state = () => ({
trusted_instances_label: '', trusted_instances_label: '',
footerLinks: [] footerLinks: []
}, },
filter: {
query: '',
show_recurrent: null,
show_multidate: null
},
announcements: [], announcements: [],
events: [] events: []
}) })
@ -39,6 +44,9 @@ export const mutations = {
}, },
setEvents (state, events) { setEvents (state, events) {
state.events = events state.events = events
},
setFilter (state, { type, value }) {
state.filter[type] = value
} }
} }
@ -47,6 +55,9 @@ export const actions = {
// we use it to get configuration from db, set locale, etc... // we use it to get configuration from db, set locale, etc...
nuxtServerInit ({ commit }, { _req, res }) { nuxtServerInit ({ commit }, { _req, res }) {
commit('setSettings', res.locals.settings) commit('setSettings', res.locals.settings)
commit('setFilter', { type: 'show_recurrent',
value: res.locals.settings.allow_recurrent_event && res.locals.settings.recurrent_event_visible })
if (res.locals.status === 'READY') { if (res.locals.status === 'READY') {
commit('setAnnouncements', res.locals.announcements) commit('setAnnouncements', res.locals.announcements)
} }
@ -62,11 +73,15 @@ export const actions = {
await this.$axios.$post('/settings', setting) await this.$axios.$post('/settings', setting)
commit('setSetting', setting) commit('setSetting', setting)
}, },
setFilter ({ commit }, [type, value]) {
commit('setFilter', { type, value })
},
async getEvents ({ commit, state }, params = {}) { async getEvents ({ commit, state }, params = {}) {
const events = await this.$api.getEvents({ const events = await this.$api.getEvents({
start: params.start || dayjs().startOf('month').unix(), start: params.start || dayjs().startOf('month').unix(),
end: params.end || null, end: params.end || null,
show_recurrent: params.show_recurrent || state.settings.recurrent_event_visible show_recurrent: state.filter.show_recurrent,
show_multidate: state.filter.show_multidate
}) })
commit('setEvents', events) commit('setEvents', events)
return events return events