mirror of
https://framagit.org/les/gancio.git
synced 2025-01-31 16:42:22 +01:00
feat: add Exclusion filters in collections, fix #393
This commit is contained in:
parent
ac592ac001
commit
dc8294d15e
4 changed files with 81 additions and 45 deletions
|
@ -28,7 +28,7 @@ v-container
|
|||
|
||||
v-btn(color='primary' text @click='newCollection') <v-icon v-text='mdiPlus'></v-icon> {{ $t('admin.new_collection') }}
|
||||
|
||||
v-dialog(v-model='dialog' width='900' destroy-on-close :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-dialog(v-model='dialog' max-width='8000' width='800' destroy-on-close :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{ $t('admin.edit_collection') }}
|
||||
v-card-text
|
||||
|
@ -45,7 +45,7 @@ v-container
|
|||
h3(v-else class='text-h5' v-text='collection.name')
|
||||
|
||||
v-row
|
||||
v-col(cols=4)
|
||||
v-col(cols=6)
|
||||
//- @input.native='searchActors'
|
||||
//- @focus='searchActors'
|
||||
v-autocomplete(v-model='filterActors'
|
||||
|
@ -55,6 +55,7 @@ v-container
|
|||
:disabled="!collection.id"
|
||||
placeholder='Local'
|
||||
return-object
|
||||
hide-details
|
||||
item-value='ap_id'
|
||||
item-text='ap_id'
|
||||
:delimiters="[',', ';']"
|
||||
|
@ -70,23 +71,7 @@ v-container
|
|||
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
|
||||
:input-value="selected" label small) @{{ item?.object?.preferredUsername }}@{{ item?.instanceDomain }}
|
||||
|
||||
v-col(cols=4)
|
||||
v-autocomplete(v-model='filterTags'
|
||||
cache-items
|
||||
:prepend-inner-icon="mdiTagMultiple"
|
||||
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
|
||||
:disabled="!collection.id"
|
||||
placeholder='All'
|
||||
@input.native='searchTags'
|
||||
@focus='searchTags'
|
||||
:delimiters="[',', ';']"
|
||||
:items="tags"
|
||||
:label="$t('common.tags')")
|
||||
template(v-slot:selection="{ item, on, attrs, selected, parent }")
|
||||
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
|
||||
:input-value="selected" label small) {{ item }}
|
||||
|
||||
v-col(cols=4)
|
||||
v-col(cols=6)
|
||||
v-autocomplete(v-model='filterPlaces'
|
||||
cache-items
|
||||
:prepend-inner-icon="mdiMapMarker"
|
||||
|
@ -94,6 +79,7 @@ v-container
|
|||
auto-select-first
|
||||
clearable
|
||||
return-object
|
||||
hide-details
|
||||
item-text='name'
|
||||
:disabled="!collection.id"
|
||||
@input.native="searchPlaces"
|
||||
|
@ -105,12 +91,32 @@ v-container
|
|||
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
|
||||
:input-value="selected" label small) {{ item.name }}
|
||||
|
||||
v-col(cols=6)
|
||||
v-autocomplete(v-model='filterTags'
|
||||
cache-items
|
||||
:prepend-inner-icon="mdiTagMultiple"
|
||||
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
|
||||
:disabled="!collection.id"
|
||||
hide-details
|
||||
placeholder='All'
|
||||
@input.native='searchTags'
|
||||
@focus='searchTags'
|
||||
:delimiters="[',', ';']"
|
||||
:items="tags"
|
||||
:label="$t('common.tags')")
|
||||
template(v-slot:selection="{ item, on, attrs, selected, parent }")
|
||||
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
|
||||
:input-value="selected" label small) {{ item }}
|
||||
|
||||
|
||||
|
||||
//- template(v-slot:item="{ item, attrs, on }")
|
||||
//- v-list-item(v-bind='attrs' v-on='on')
|
||||
//- v-list-item-content(two-line)
|
||||
//- v-list-item-title(v-text='item.name')
|
||||
//- v-list-item-subtitle(v-text='item.address')
|
||||
|
||||
v-col(cols=3)
|
||||
v-switch(inset v-model='negateFilter' label='Not')
|
||||
|
||||
v-data-table(
|
||||
:headers='filterHeaders'
|
||||
|
@ -121,6 +127,8 @@ v-container
|
|||
template(v-slot:item.actions='{ item }')
|
||||
v-btn(@click='removeFilter(item)' color='error' icon)
|
||||
v-icon(v-text='mdiDeleteForever')
|
||||
template(v-slot:item.negate='{ item }')
|
||||
v-icon(v-text='item.negate ? mdiNotEqualVariant : mdiEqualBox' :color='item.negate ? "warning" : "success"')
|
||||
template(v-slot:item.tags='{ item }')
|
||||
v-chip.ma-1(small label v-for='tag in item.tags' v-text='tag' :key='tag')
|
||||
template(v-slot:item.places='{ item }')
|
||||
|
@ -131,8 +139,8 @@ v-container
|
|||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='primary' outlined :loading='loading' text @click='addFilter' :disabled='loading || !filterActors.length && !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
|
||||
v-btn(@click='dialog = false' outlined color='warning' :disabled="loading || filterActors.length || filterPlaces.length || filterTags.length") {{ $t('common.close') }}
|
||||
v-btn(color='primary' outlined :loading='loading' text @click='addFilter' :disabled='loading || filterActors.length<1 && filterPlaces.length<1 && filterTags.length<1') add <v-icon v-text='mdiPlus'></v-icon>
|
||||
v-btn(@click='dialog = false' outlined color='warning' :disabled="loading || filterActors.length>0 || filterPlaces.length>0 || filterTags.length>0") {{ $t('common.close') }}
|
||||
|
||||
v-card-text
|
||||
v-data-table(
|
||||
|
@ -160,12 +168,14 @@ import debounce from 'lodash/debounce'
|
|||
import isEqual from 'lodash/isEqual'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown, mdiWeb } from '@mdi/js'
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever,
|
||||
mdiCloseCircle, mdiChevronDown, mdiWeb, mdiInformation, mdiNotEqualVariant, mdiEqualBox } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
data({ $store }) {
|
||||
return {
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown, mdiWeb,
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever,
|
||||
mdiCloseCircle, mdiChevronDown, mdiWeb, mdiInformation, mdiEqualBox, mdiNotEqualVariant,
|
||||
loading: false,
|
||||
dialog: false,
|
||||
valid: false,
|
||||
|
@ -173,6 +183,7 @@ export default {
|
|||
filterTags: [],
|
||||
filterPlaces: [],
|
||||
filterActors: [],
|
||||
negateFilter: false,
|
||||
actors: [],
|
||||
tags: [],
|
||||
places: [],
|
||||
|
@ -187,6 +198,7 @@ export default {
|
|||
{ value: 'actions', text: this.$t('common.actions'), align: 'right', width: 150, sortable: false }
|
||||
],
|
||||
filterHeaders: [
|
||||
{ value: 'negate', text: '', width: 20, sortable: false },
|
||||
{ value: 'actors', text: this.$t('common.actors') },
|
||||
{ value: 'tags', text: this.$t('common.tags') },
|
||||
{ value: 'places', text: this.$t('common.places') },
|
||||
|
@ -235,7 +247,7 @@ export default {
|
|||
const tags = this.filterTags
|
||||
const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name }))
|
||||
const actors = this.filterActors.map(a => ({ ap_id: a.ap_id, name: a.object?.preferredUsername ?? a.object?.username, domain: a.instanceDomain }))
|
||||
const filter = { collectionId: this.collection.id, tags, places, actors }
|
||||
const filter = { collectionId: this.collection.id, tags, places, actors, negate: this.negateFilter }
|
||||
|
||||
// tags and places are JSON field and there's no way to use them inside a unique constrain
|
||||
const alreadyExists = this.filters.find(f =>
|
||||
|
@ -244,7 +256,10 @@ export default {
|
|||
isEqual(sortBy(f.actors), sortBy(filter.actors))
|
||||
)
|
||||
|
||||
if (alreadyExists) return
|
||||
if (alreadyExists) {
|
||||
this.$root.$message('Already exists', { color: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
const ret = await this.$axios.$post('/filter', filter )
|
||||
this.$fetch()
|
||||
|
@ -252,6 +267,7 @@ export default {
|
|||
this.filterTags = []
|
||||
this.filterPlaces = []
|
||||
this.filterActors = []
|
||||
this.negateFilter = false
|
||||
this.loading = false
|
||||
},
|
||||
async editCollection(collection) {
|
||||
|
|
|
@ -45,10 +45,10 @@ const collectionController = {
|
|||
async getEvents (req, res) {
|
||||
const settings = res.locals.settings
|
||||
const exportController = require('./export')
|
||||
const format = req.params.format || 'json'
|
||||
const format = req.params?.format ?? 'json'
|
||||
const name = req.params.name
|
||||
const limit = req.query.max
|
||||
const start = req.query.start_at || DateTime.local().toUnixInteger()
|
||||
const limit = req.query?.max ?? 10
|
||||
const start = req.query?.start_at ?? DateTime.local().toUnixInteger()
|
||||
const reverse = queryParamToBool(req.query.reverse)
|
||||
const older = queryParamToBool(req.query.older)
|
||||
const show_recurrent = settings.allow_recurrent_event && queryParamToBool(req.query.show_recurrent, settings.recurrent_event_visible)
|
||||
|
@ -78,15 +78,17 @@ const collectionController = {
|
|||
async _getEvents ({
|
||||
name, start, end,
|
||||
show_recurrent=false,
|
||||
limit, include_description=false,
|
||||
limit=10, include_description=false,
|
||||
older, reverse }) {
|
||||
|
||||
// get the collection from specified name
|
||||
const collection = await Collection.findOne({ where: { name } })
|
||||
if (!collection) {
|
||||
log.warn(`[COLLECTION] "%s" not found`, name)
|
||||
return []
|
||||
}
|
||||
|
||||
// and all related filters
|
||||
const filters = await Filter.findAll({ where: { collectionId: collection.id } })
|
||||
|
||||
// collection is empty if there are no filters
|
||||
|
@ -118,32 +120,40 @@ const collectionController = {
|
|||
}
|
||||
|
||||
const replacements = []
|
||||
const ors = []
|
||||
const conditions = []
|
||||
const negatedConditions = []
|
||||
|
||||
// collections are a set of filters to match
|
||||
filters.forEach(f => {
|
||||
|
||||
let conditions = []
|
||||
let tmpConditions = []
|
||||
|
||||
if (f.tags && f.tags.length) {
|
||||
const tags = Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=event.id AND ${Col('tagTag')} in (?)`))
|
||||
replacements.push(f.tags)
|
||||
conditions.push(tags)
|
||||
tmpConditions.push(tags)
|
||||
}
|
||||
|
||||
if (f.places && f.places.length) {
|
||||
conditions.push({ placeId: f.places.map(p => p.id) })
|
||||
tmpConditions.push({ placeId: f.places.map(p => p.id) })
|
||||
}
|
||||
|
||||
if (f.actors && f.actors.length) {
|
||||
conditions.push({ apUserApId: f.actors.map(a => a.ap_id)})
|
||||
tmpConditions.push({ apUserApId: f.actors.map(a => a.ap_id)})
|
||||
}
|
||||
|
||||
if (!conditions.length) return
|
||||
ors.push(conditions.length === 1 ? conditions[0] : { [Op.and]: conditions })
|
||||
if (!tmpConditions.length) return
|
||||
if (f.negate) {
|
||||
negatedConditions.push(tmpConditions.length === 1 ? tmpConditions[0] : { [Op.and]: tmpConditions })
|
||||
} else {
|
||||
conditions.push(tmpConditions.length === 1 ? tmpConditions[0] : { [Op.and]: tmpConditions })
|
||||
}
|
||||
})
|
||||
|
||||
where[Op.and] = { [Op.or]: ors }
|
||||
where[Op.and] = {
|
||||
...(negatedConditions.length > 0 && { [Op.not]: negatedConditions}),
|
||||
...(conditions.length > 0 && { [Op.or]: conditions })
|
||||
}
|
||||
|
||||
const events = await Event.findAll({
|
||||
where,
|
||||
|
@ -213,14 +223,10 @@ const collectionController = {
|
|||
},
|
||||
|
||||
async addFilter (req, res) {
|
||||
const { collectionId, tags, places, actors } = req.body
|
||||
const { collectionId, tags, places, actors, negate } = req.body
|
||||
|
||||
try {
|
||||
// if (actors?.length) {
|
||||
// const actors_to_follow = await APUser.findAll({ where: { ap_id: { [Op.in]: actors.map(a => a.ap_id) }} })
|
||||
// await Promise.all(actors_to_follow.map(followActor))
|
||||
// }
|
||||
const filter = await Filter.create({ collectionId, tags, places, actors })
|
||||
const filter = await Filter.create({ collectionId, tags, places, actors, negate })
|
||||
return res.json(filter)
|
||||
} catch (e) {
|
||||
log.error(String(e))
|
||||
|
|
|
@ -6,6 +6,9 @@ module.exports = (sequelize, DataTypes) =>
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
negate: {
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
},
|
||||
|
|
11
server/migrations/20240517093747-filter_collection_negate.js
Normal file
11
server/migrations/20240517093747-filter_collection_negate.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up (queryInterface, Sequelize) {
|
||||
return queryInterface.addColumn('filters', 'negate', { type: Sequelize.BOOLEAN })
|
||||
},
|
||||
|
||||
async down (queryInterface, Sequelize) {
|
||||
return queryInterface.removeColumn('filters', 'negate')
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue