feat: user events page view, fix #340 and #156

This commit is contained in:
lesion 2024-01-29 14:46:11 +01:00
parent 9be6fbc19c
commit 2492f6b545
No known key found for this signature in database
GPG key ID: 352918250B012177
7 changed files with 171 additions and 45 deletions

View file

@ -15,7 +15,7 @@ li {
} }
.v-dialog .theme--dark.v-card { .v-dialog .theme--dark.v-card {
background-color: #1e1e1e; background-color: #333;
} }
.v-application { .v-application {

20
components/TBtn.vue Normal file
View file

@ -0,0 +1,20 @@
<template>
<v-tooltip top>
<template v-slot:activator="{ on, attrs}">
<v-btn v-bind="attrs" :to='to' icon v-on="on" @click='$emit("click")' :color='color'>
<slot />
</v-btn>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</template>
<script>
export default {
name: 'TBtn',
props: {
tooltip: String,
color: String,
to: String
}
}
</script>

View file

@ -109,7 +109,8 @@
"pin": "Pin", "pin": "Pin",
"trusted_instances": "Trusted instances", "trusted_instances": "Trusted instances",
"actors": "Node", "actors": "Node",
"collection_in_home": "Show a collection in home" "collection_in_home": "Show a collection in home",
"my_events": "My Events"
}, },
"login": { "login": {
"description": "By logging in you can publish new events.", "description": "By logging in you can publish new events.",

View file

@ -1,54 +1,119 @@
<template lang="pug"> <template>
v-container <v-container class="pa-0 pa-md-3">
v-card <v-card>
v-card-title.text-h5 {{$auth.user.email}} <v-card-title>{{$auth.user.email}}</v-card-title>
v-card-text <v-card-text>
p {{$t('settings.remove_account')}} <v-tabs v-model='selectedTab'>
v-btn.black--text(color='warning' @click='remove_account') {{$t('common.remove')}} <v-tab href="#mine">{{$t('common.my_events')}}</v-tab>
<v-tab href='#settings'>{{$t('common.settings')}}</v-tab>
<v-tab-item value="mine">
<v-container>
<v-card-text class="pa-0 pa-md-3">
<v-text-field v-model="search" :label="$t('common.search')"/>
<v-data-table :items="events" :hide-default-footer='events.length<10' dense :search="search"
:header-props='{ sortIcon: mdiChevronDown }'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:headers='headers'>
<template v-slot:item.data-table-select="{ isSelected, select }">
<v-simple-checkbox size='small' small dense :on-icon="mdiCheckboxOutline" :off-icon="mdiCheckboxBlankOutline" :value="isSelected" @input="select($event)" />
</template>
<template v-slot:item.when='{ item }'>
<span v-if='!item.recurrent'>{{$time.when(item)}}</span>
<span v-else><v-icon color='success' v-text='mdiRepeat' /> {{$time.recurrentDetail({ parent: item }, 'EEEE, HH:mm')}}</span>
</template>
<template v-slot:item.actions='{ item }'>
<template v-if="!item.recurrent">
<t-btn @click='toggle(item)' v-if='!item.is_visible' color='success' :tooltip="$t('common.confirm')"><v-icon v-text='mdiEye' /></t-btn>
<t-btn @click='toggle(item)' v-else-if="!item.parentId" color='info' :tooltip="$t('common.hide')"><v-icon v-text='mdiEyeOff' /></t-btn>
<t-btn @click='toggle(item)' v-else color='info' :tooltip="$t('common.skip')"><v-icon v-text='mdiDebugStepOver' /></t-btn>
<t-btn :to='`/event/${item.slug || item.id}`' :tooltip="$t('common.preview')"><v-icon v-text='mdiArrowRight' /></t-btn>
</template>
<template v-else>
<t-btn @click='toggle(item)' v-if='!item.is_visible' color='success' :tooltip="$t('common.start')"><v-icon v-text='mdiPlay' /></t-btn>
<t-btn @click='toggle(item)' v-else color='info' :tooltip="$t('common.pause')"><v-icon v-text='mdiPause' /></t-btn>
</template>
<t-btn :to='`/add/${item.id}`' color='warning' :tooltip="$t('common.edit')"><v-icon v-text='mdiPencil' /></t-btn>
<t-btn @click='remove(item, item.recurrent)' color='error' :tooltip="$t('common.delete')"> <v-icon v-text='item.recurrent ? mdiDeleteForever : mdiDelete' /></t-btn>
</template>
</v-data-table>
</v-card-text>
</v-container>
</v-tab-item>
<v-tab-item value='settings'>
<v-container>
<v-btn @click='forgot'>{{$t('login.forgot_password')}}</v-btn><br/><br/>
<v-divider />
<p>{{$t('settings.remove_account')}}</p>
<v-btn color='warning' @click='remove_account'>{{$t('common.remove')}}</v-btn>
</v-container>
</v-tab-item>
</v-tabs>
</v-card-text>
</v-card>
</v-container>
</template> </template>
<script> <script>
import { mdiChevronLeft, mdiChevronRight, mdiChevronDown, mdiCheckboxOutline, mdiCheckboxBlankOutline,
mdiPencil, mdiDelete, mdiArrowRight, mdiEye, mdiEyeOff, mdiRepeat, mdiPause, mdiPlay, mdiDebugStepOver, mdiDeleteForever } from '@mdi/js'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import TBtn from '../components/TBtn.vue'
export default { export default {
name: 'Settings', name: 'Settings',
middleware: ['auth'], middleware: ['auth'],
components: { TBtn },
data () { data () {
return { return {
password: '', mdiChevronLeft, mdiChevronRight, mdiChevronDown, mdiCheckboxOutline, mdiCheckboxBlankOutline,
user: { } mdiPencil, mdiDelete, mdiArrowRight, mdiEye, mdiEyeOff, mdiRepeat, mdiPause, mdiPlay, mdiDebugStepOver, mdiDeleteForever,
selectedTab: 'mine',
events: [],
selectedEvents: [],
search: '',
headers: [
{ value: 'title', text: this.$t('common.title') },
{ value: 'place.name', text: this.$t('common.place') },
{ value: 'when', text: this.$t('common.when') },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
} }
}, },
computed: mapState(['settings']), computed: mapState(['settings']),
async fetch () {
this.events = await this.$axios.$get('/events/mine')
},
methods: { methods: {
// async change_password () { async forgot () {
// if (!this.password) { return } this.loading = true
// const user_data = { id: this.$auth.user.id, password: this.password } await this.$axios.$post('/user/recover', { email: this.$auth.user.email })
// try { this.loading = false
// await this.$axios.$put('/user', user_data) this.$root.$message('login.check_email', { color: 'success' })
// Message({ message: this.$t('settings.password_updated'), showClose: true, type: 'success' }) },
// this.$router.replace('/')
// } catch (e) {
// console.log(e)
// }
// },
// update_settings () {
// MessageBox.confirm(this.$t('settings.update_confirm'),
// this.$t('common.confirm'), {
// confirmButtonText: this.$t('common.ok'),
// cancelButtonText: this.$t('common.cancel'),
// type: 'error'
// }).then(async () => {
// this.user = await this.$axios.$put('/user', { ...this.user, password: this.password })
// }).catch(e => {
// Message({ message: e, showClose: true, type: 'warning' })
// })
// },
async remove_account () { async remove_account () {
const ret = await this.$root.$confirm('settings.remove_account_confirm', { color: 'error' }) const ret = await this.$root.$confirm('settings.remove_account_confirm', { color: 'error' })
if (!ret) return if (!ret) return
this.$axios.$delete('/user') this.$axios.$delete('/user')
this.$auth.logout() this.$auth.logout()
this.$router.replace('/') this.$router.replace('/')
},
async toggle (event) {
const id = event.id
const is_visible = event.is_visible
const method = is_visible ? 'unconfirm' : 'confirm'
try {
await this.$axios.$put(`/event/${method}/${id}`)
event.is_visible = !is_visible
} catch (e) {
console.error(e)
}
},
async remove (event, parent) {
const ret = await this.$root.$confirm(`event.remove_${parent ? 'recurrent_' : ''}confirmation`)
if (!ret) { return }
await this.$axios.delete(`/event/${event.id}`)
this.$fetch()
this.$root.$message('admin.event_remove_ok')
} }
}, },
head () { head () {

View file

@ -88,7 +88,7 @@ export default ({ app, store }, inject) => {
endOfDay (date) { return DateTime.fromJSDate(date, { zone }).endOf('day').toUnixInteger()}, endOfDay (date) { return DateTime.fromJSDate(date, { zone }).endOf('day').toUnixInteger()},
recurrentDetail (event) { recurrentDetail (event, format='EEEE') {
const opt = { const opt = {
zone, zone,
locale: app.i18n.locale || store.state.settings.instance_locale locale: app.i18n.locale || store.state.settings.instance_locale
@ -98,9 +98,9 @@ export default ({ app, store }, inject) => {
const { frequency, type } = parent.recurrent const { frequency, type } = parent.recurrent
let recurrent let recurrent
if (frequency === '1w' || frequency === '2w') { if (frequency === '1w' || frequency === '2w') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: DateTime.fromSeconds(parent.start_datetime, opt).toFormat('EEEE')}) recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: DateTime.fromSeconds(parent.start_datetime, opt).toFormat(format)})
} else if (frequency === '1m' || frequency === '2m') { } else if (frequency === '1m' || frequency === '2m') {
const d = type === 'ordinal' ? DateTime.fromSeconds(parent.start_datetime, opt).day : DateTime.fromSeconds(parent.start_datetime, opt).toFormat('EEEE') const d = type === 'ordinal' ? DateTime.fromSeconds(parent.start_datetime, opt).day : DateTime.fromSeconds(parent.start_datetime, opt).toFormat(format)
if (type === 'ordinal') { if (type === 'ordinal') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d }) recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d })
} else { } else {

View file

@ -437,8 +437,13 @@ const eventController = {
try { try {
const body = req.body const body = req.body
const event = await Event.findByPk(body.id) const event = await Event.findByPk(body.id)
if (!event) { return res.sendStatus(404) } if (!event) {
log.debug('[UPDATE] Event not found: %s', body?.id)
return res.sendStatus(404)
}
if (!req.user.is_admin && event.userId !== req.user.id) { if (!req.user.is_admin && event.userId !== req.user.id) {
log.debug('[UPDATE] the user is neither an admin nor the owner of the event')
return res.sendStatus(403) return res.sendStatus(403)
} }
@ -448,19 +453,23 @@ const eventController = {
// // validate start_datetime and end_datetime // // validate start_datetime and end_datetime
if (end_datetime) { if (end_datetime) {
if (start_datetime > end_datetime) { if (start_datetime > end_datetime) {
log.debug('[UPDATE] start_datetime is greated than end_datetime')
return res.status(400).send(`start datetime is greater than end datetime`) return res.status(400).send(`start datetime is greater than end datetime`)
} }
if (Number(end_datetime) > 1000*24*60*60*365) { if (Number(end_datetime) > 1000*24*60*60*365) {
log.debug('[UPDATE] end_datetime is too much in the future')
return res.status(400).send('are you sure?') return res.status(400).send('are you sure?')
} }
} }
if (!Number(start_datetime)) { if (!Number(start_datetime)) {
log.debug('[UPDATE] start_datetime has to be a number')
return res.status(400).send(`Wrong format for start datetime`) return res.status(400).send(`Wrong format for start datetime`)
} }
if (Number(start_datetime) > 1000*24*60*60*365) { if (Number(start_datetime) > 1000*24*60*60*365) {
log.debug('[UPDATE] start_datetime is too much in the future')
return res.status(400).send('are you sure?') return res.status(400).send('are you sure?')
} }
@ -523,9 +532,11 @@ const eventController = {
try { try {
place = await eventController._findOrCreatePlace(body) place = await eventController._findOrCreatePlace(body)
if (!place) { if (!place) {
log.info('[UPDATE] Place not found')
return res.status(400).send(`Place not found`) return res.status(400).send(`Place not found`)
} }
} catch (e) { } catch (e) {
log.info('[UPDATE] %s', e?.message ?? String(e))
return res.status(400).send(e.message) return res.status(400).send(e.message)
} }
await event.setPlace(place) await event.setPlace(place)
@ -618,14 +629,12 @@ const eventController = {
page, page,
older, older,
reverse, reverse,
user_id,
include_unconfirmed = false,
include_parent = false,
include_description=false }) { include_description=false }) {
const where = { const where = {
// do not include _parent_ recurrent event
recurrent: null,
// confirmed event only
is_visible: true,
apUserApId: null, apUserApId: null,
@ -635,6 +644,20 @@ const eventController = {
} }
} }
if (user_id) {
where.userId = user_id
}
if (include_parent !== true) {
// do not include _parent_ recurrent event
where.recurrent = null
}
if (include_unconfirmed !== true) {
// confirmed event only
where.is_visible = true
}
// include recurrent events? // include recurrent events?
if (!show_recurrent) { if (!show_recurrent) {
where.parentId = null where.parentId = null
@ -689,7 +712,12 @@ const eventController = {
const events = await Event.findAll({ const events = await Event.findAll({
where, where,
attributes: { attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'resources', 'recurrent', 'placeId', 'image_path', ...(!include_description ? ['description']: [])] exclude: [
'likes', 'boost', 'userId', 'createdAt', 'resources', 'placeId', 'image_path',
...(!include_parent ? ['recurrent']: []),
...(!include_unconfirmed ? ['is_visible']: []),
...(!include_description ? ['description']: [])
]
}, },
order: [['start_datetime', reverse ? 'DESC' : 'ASC']], order: [['start_datetime', reverse ? 'DESC' : 'ASC']],
include: [ include: [
@ -715,6 +743,17 @@ const eventController = {
}) })
}, },
async mine (req, res) {
const events = await eventController._select({
user_id: req.user.id,
include_parent: true,
include_unconfirmed: true,
show_recurrent: true,
show_multidate: true
})
return res.json(events)
},
/** /**
* Select events based on params * Select events based on params
*/ */

View file

@ -117,6 +117,7 @@ module.exports = () => {
* [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42) * [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
*/ */
api.get('/events/mine', isAuth, eventController.mine)
api.get('/events', cors, eventController.select) api.get('/events', cors, eventController.select)
/** /**