Allow to accept / reject participants

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-09-20 18:22:03 +02:00
parent ffa4ec9209
commit abf3a58657
31 changed files with 1208 additions and 299 deletions

View File

@@ -1,3 +1,4 @@
import {EventJoinOptions} from "@/types/event.model";
<template>
<section class="container">
<h1 class="title" v-if="isUpdate === false">
@@ -187,11 +188,17 @@
</style>
<script lang="ts">
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT, FETCH_EVENTS } from '@/graphql/event';
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration, IEvent } from '@/types/event.model';
import {
CommentModeration, EventJoinOptions,
EventModel,
EventStatus,
EventVisibility,
EventVisibilityJoinOptions,
} from '@/types/event.model';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
import { Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue';
import Editor from '@/components/Editor.vue';
import DateTimePicker from '@/components/Event/DateTimePicker.vue';
@@ -352,6 +359,15 @@ export default class EditEvent extends Vue {
}
}
@Watch('needsApproval')
updateEventJoinOptions(needsApproval) {
if (needsApproval === true) {
this.event.joinOptions = EventJoinOptions.RESTRICTED;
} else {
this.event.joinOptions = EventJoinOptions.FREE;
}
}
// getAddressData(addressData) {
// if (addressData !== null) {
// this.event.address = {

View File

@@ -103,7 +103,7 @@
</b-modal>
</div>
<div class="organizer">
<actor-link :actor="event.organizerActor">
<span>
<span v-if="event.organizerActor">
{{ $t('By {name}', {name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}) }}
</span>
@@ -113,31 +113,11 @@
:src="event.organizerActor.avatar.url"
:alt="event.organizerActor.avatar.alt" />
</figure>
</actor-link>
</span>
</div>
</div>
</div>
</section>
<!-- <p v-if="actorIsOrganizer()">-->
<!-- <translate>You are an organizer.</translate>-->
<!-- </p>-->
<!-- <div v-else>-->
<!-- <p v-if="actorIsParticipant()">-->
<!-- <translate>You announced that you're going to this event.</translate>-->
<!-- </p>-->
<!-- <p v-else>-->
<!-- <translate>Are you going to this event?</translate><br />-->
<!-- <span>-->
<!-- <translate-->
<!-- :translate-n="event.participants.length"-->
<!-- translate-plural="{event.participants.length} persons are going"-->
<!-- >-->
<!-- One person is going.-->
<!-- </translate>-->
<!-- </span>-->
<!-- </p>-->
<!-- </div>-->
<div class="description">
<div class="description-container container">
<h3 class="title">
@@ -147,63 +127,31 @@
{{ $t("The event organizer didn't add any description.") }}
</p>
<div class="columns" v-else>
<div class="column is-half">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse vehicula ex dapibus augue volutpat, ultrices cursus mi rutrum.
Nunc ante nunc, facilisis a tellus quis, tempor mollis diam. Aenean consectetur quis est a ultrices.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<p><a href="https://framasoft.org">https://framasoft.org</a>
<p>
Nam sit amet est eget velit tristique commodo. Etiam sollicitudin dignissim diam, ut ultricies tortor.
Sed quis blandit diam, a tincidunt nunc. Donec tincidunt tristique neque at rhoncus. Ut eget vulputate felis.
Pellentesque nibh purus, viverra ac augue sed, iaculis feugiat velit. Nulla ut hendrerit elit.
Etiam at justo eu nunc tempus sagittis. Sed ac tincidunt tellus, sit amet luctus velit.
Nam ullamcorper eros eleifend, eleifend diam vitae, lobortis risus.
</p>
<p>
<em>
Curabitur rhoncus sapien tortor, vitae imperdiet massa scelerisque non.
Aliquam eu augue mi. Donec hendrerit lorem orci.
</em>
</p>
<p>
Donec volutpat, enim eu laoreet dictum, urna quam varius enim, eu convallis urna est vitae massa.
Morbi porttitor lacus a sem efficitur blandit. Mauris in est in quam tincidunt iaculis non vitae ipsum.
Phasellus eget velit tellus. Curabitur ac neque pharetra velit viverra mollis.
</p>
<img src="https://framasoft.org/img/biglogo-notxt.png" alt="logo Framasoft"/>
<p>Aenean gravida, ante vitae aliquet aliquet, elit quam tristique orci, sit amet dictum lorem ipsum nec tortor.
Vestibulum est eros, faucibus et semper vel, dapibus ac est. Suspendisse potenti. Suspendisse potenti.
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
Nulla molestie nisi ac risus hendrerit, dapibus mattis sapien scelerisque.
</p>
<p>Maecenas id pretium justo, nec dignissim sapien. Mauris in venenatis odio, in congue augue. </p>
<div class="column is-half" v-html="event.description">
</div>
</div>
</div>
</div>
<!-- <section class="container">-->
<!-- <h2 class="title">Participants</h2>-->
<!-- <span v-if="event.participants.length === 0">No participants yet.</span>-->
<!-- <div class="columns">-->
<!-- <router-link-->
<!-- class="column"-->
<!-- v-for="participant in event.participants"-->
<!-- :key="participant.preferredUsername"-->
<!-- :to="{name: 'Profile', params: { name: participant.actor.preferredUsername }}"-->
<!-- >-->
<!-- <div>-->
<!-- <figure>-->
<!-- <img v-if="!participant.actor.avatar.url" src="https://picsum.photos/125/125/">-->
<!-- <img v-else :src="participant.actor.avatar.url">-->
<!-- </figure>-->
<!-- <span>{{ participant.actor.preferredUsername }}</span>-->
<!-- </div>-->
<!-- </router-link>-->
<!-- </div>-->
<!-- </section>-->
<section class="container">
<h3 class="title">{{ $t('Participants') }}</h3>
<router-link v-if="currentActor.id === event.organizerActor.id" :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: event.uuid } }">
{{ $t('Manage participants') }}
</router-link>
<span v-if="event.participants.length === 0">{{ $t('No participants yet.') }}</span>
<div class="columns">
<div
class="column"
v-for="participant in event.participants"
:key="participant.id"
>
<figure class="image is-48x48">
<img v-if="!participant.actor.avatar.url" src="https://picsum.photos/48/48/" class="is-rounded">
<img v-else :src="participant.actor.avatar.url" class="is-rounded">
</figure>
<span>{{ participant.actor.preferredUsername }}</span>
</div>
</div>
</section>
<section class="share">
<div class="container">
<div class="columns">
@@ -236,7 +184,7 @@
</div>
</section>
<b-modal :active.sync="isReportModalActive" has-modal-card ref="reportModal">
<report-modal :on-confirm="reportEvent" title="Report this event" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" />
<report-modal :on-confirm="reportEvent" :title="$t('Report this event')" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" />
</b-modal>
<b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal">
<participation-modal :on-confirm="joinEvent" :event="event" :defaultIdentity="currentActor" @close="$refs.participationModal.close()" />
@@ -249,7 +197,7 @@
import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { EventVisibility, IEvent, IParticipant } from '@/types/event.model';
import { EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson } from '@/types/actor';
import { RouteName } from '@/router';
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
@@ -263,6 +211,7 @@ import ParticipationModal from '@/components/Event/ParticipationModal.vue';
import { IReport } from '@/types/report.model';
import { CREATE_REPORT } from '@/graphql/report';
import EventMixin from '@/mixins/event';
import { EventRouteName } from '@/router/event';
@Component({
components: {
@@ -283,6 +232,7 @@ import EventMixin from '@/mixins/event';
variables() {
return {
uuid: this.uuid,
roles: [ParticipantRole.CREATOR, ParticipantRole.MODERATOR, ParticipantRole.MODERATOR, ParticipantRole.PARTICIPANT].join(),
};
},
},
@@ -302,6 +252,7 @@ export default class Event extends EventMixin {
isJoinModalActive: boolean = false;
EventVisibility = EventVisibility;
EventRouteName = EventRouteName;
/**
* Delete the event, then redirect to home.
@@ -367,9 +318,10 @@ export default class Event extends EventMixin {
confirmLeave() {
this.$buefy.dialog.confirm({
title: `Leaving event « ${this.event.title} »`,
message: `Are you sure you want to leave event « ${this.event.title} »`,
confirmText: 'Leave event',
title: this.$t('Leaving event "{title}"', { title: this.event.title }) as string,
message: this.$t('Are you sure you want to cancel your participation at event "{title}"?', { title: this.event.title }) as string,
confirmText: this.$t('Leave event') as string,
cancelText: this.$t('Cancel') as string,
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.leaveEvent(),

View File

@@ -0,0 +1,197 @@
<template>
<main class="container">
<b-tabs type="is-boxed" v-if="event">
<b-tab-item>
<template slot="header">
<b-icon icon="information-outline"></b-icon>
<span> Participants <b-tag rounded> {{ participantStats.approved }} </b-tag> </span>
</template>
<section v-if="participantsAndCreators.length > 0">
<h2 class="title">{{ $t('Participants') }}</h2>
<div class="columns">
<div class="column is-one-quarter-desktop" v-for="participant in participantsAndCreators" :key="participant.actor.id">
<participant-card
:participant="participant"
:accept="acceptParticipant"
:reject="refuseParticipant"
:exclude="refuseParticipant"
/>
</div>
</div>
</section>
</b-tab-item>
<b-tab-item>
<template slot="header">
<b-icon icon="source-pull"></b-icon>
<span> Demandes <b-tag rounded> {{ participantStats.unapproved }} </b-tag> </span>
</template>
<section v-if="queue.length > 0">
<h2 class="title">{{ $t('Waiting list') }}</h2>
<div class="columns">
<div class="column is-one-quarter-desktop" v-for="participant in queue" :key="participant.actor.id">
<participant-card
:participant="participant"
:accept="acceptParticipant"
:reject="refuseParticipant"
:exclude="refuseParticipant"
/>
</div>
</div>
</section>
</b-tab-item>
</b-tabs>
</main>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IEvent, IParticipant, Participant, ParticipantRole } from '@/types/event.model';
import { ACCEPT_PARTICIPANT, PARTICIPANTS, REJECT_PARTICIPANT } from '@/graphql/event';
import ParticipantCard from '@/components/Account/ParticipantCard.vue';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
@Component({
components: {
ParticipantCard,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
event: {
query: PARTICIPANTS,
variables() {
return {
uuid: this.eventId,
page: 1,
limit: 10,
roles: [ParticipantRole.PARTICIPANT].join(),
};
},
},
organizers: {
query: PARTICIPANTS,
variables() {
return {
uuid: this.eventId,
page: 1,
limit: 20,
roles: [ParticipantRole.CREATOR].join(),
};
},
update: data => data.event.participants.map(participation => new Participant(participation)),
},
queue: {
query: PARTICIPANTS,
variables() {
return {
uuid: this.eventId,
page: 1,
limit: 20,
roles: [ParticipantRole.NOT_APPROVED].join(),
};
},
update: data => data.event.participants.map(participation => new Participant(participation)),
},
},
})
export default class Participants extends Vue {
@Prop({ required: true }) eventId!: string;
page: number = 1;
limit: number = 10;
// participants: IParticipant[] = [];
organizers: IParticipant[] = [];
queue: IParticipant[] = [];
event!: IEvent;
ParticipantRole = ParticipantRole;
currentActor!: IPerson;
hasMoreParticipants: boolean = false;
get participants(): IParticipant[] {
return this.event.participants.map(participant => new Participant(participant));
}
get participantStats(): Object {
return this.event.participantStats;
}
get participantsAndCreators(): IParticipant[] {
if (this.event) {
return [...this.organizers, ...this.participants];
}
return [];
}
loadMoreParticipants() {
this.page += 1;
this.$apollo.queries.participants.fetchMore({
// New variables
variables: {
page: this.page,
limit: this.limit,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newParticipations = fetchMoreResult.event.participants;
this.hasMoreParticipants = newParticipations.length === this.limit;
return {
loggedUser: {
__typename: previousResult.event.__typename,
participations: [...previousResult.event.participants, ...newParticipations],
},
};
},
});
}
async acceptParticipant(participant: IParticipant) {
try {
const { data } = await this.$apollo.mutate({
mutation: ACCEPT_PARTICIPANT,
variables: {
id: participant.id,
moderatorActorId: this.currentActor.id,
},
});
if (data) {
console.log('accept', data);
this.queue.filter(participant => participant !== data.acceptParticipation.id);
this.participants.push(participant);
}
} catch (e) {
console.error(e);
}
}
async refuseParticipant(participant: IParticipant) {
try {
const { data } = await this.$apollo.mutate({
mutation: REJECT_PARTICIPANT,
variables: {
id: participant.id,
moderatorActorId: this.currentActor.id,
},
});
if (data) {
this.participants.filter(participant => participant !== data.rejectParticipation.id);
this.queue.filter(participant => participant !== data.rejectParticipation.id);
}
} catch (e) {
console.error(e);
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
section {
padding: 3rem 0;
}
</style>

View File

@@ -107,7 +107,7 @@ export default class Group extends Vue {
}
}
</script>
<style lang="scss">
<style lang="scss" scoped>
section.container {
min-height: 30em;
}

View File

@@ -32,12 +32,13 @@
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t('Group') }}</router-link>
</b-dropdown-item>
</b-dropdown>
<section v-if="currentActor" class="container">
<section v-if="currentActor && goingToEvents.size > 0" class="container">
<h3 class="title">
{{ $t("Upcoming") }}
</h3>
<pre>{{ Array.from(goingToEvents.entries()) }}</pre>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="goingToEvents.size > 0" v-for="row in goingToEvents" class="upcoming-events">
<div v-for="row in goingToEvents" class="upcoming-events">
<span class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
<date-component :date="row[0]"></date-component>
<h3 class="subtitle"
@@ -63,9 +64,6 @@
/>
</div>
</div>
<b-message v-else type="is-danger">
{{ $t("You're not going to any event yet") }}
</b-message>
<span class="view-all">
<router-link :to=" { name: EventRouteName.MY_EVENTS }">{{ $t('View everything')}} >></router-link>
</span>
@@ -78,9 +76,10 @@
<div class="level">
<EventListCard
v-for="participation in lastWeekEvents"
:key="participation.event.uuid"
:key="participation.id"
:participation="participation"
class="level-item"
:options="{ hideDate: false }"
/>
</div>
</section>
@@ -190,6 +189,10 @@ export default class Home extends Vue {
return this.calculateDiffDays(date) < nbDays;
}
isAfter(date: string, nbDays: number) :boolean {
return this.calculateDiffDays(date) >= nbDays;
}
isInLessThanSevenDays(date: string): boolean {
return this.isBefore(date, 7);
}
@@ -200,7 +203,7 @@ export default class Home extends Vue {
get goingToEvents(): Map<string, Map<string, IParticipant>> {
const res = this.currentUserParticipations.filter(({ event }) => {
return event.beginsOn != null && !this.isBefore(event.beginsOn.toDateString(), 0);
return event.beginsOn != null && this.isAfter(event.beginsOn.toDateString(), 0) && this.isBefore(event.beginsOn.toDateString(), 7);
});
res.sort(
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime(),
@@ -208,7 +211,7 @@ export default class Home extends Vue {
return res.reduce((acc: Map<string, Map<string, IParticipant>>, participation: IParticipant) => {
const day = (new Date(participation.event.beginsOn)).toDateString();
const participations: Map<string, IParticipant> = acc.get(day) || new Map();
participations.set(participation.event.uuid, participation);
participations.set(`${participation.event.uuid}${participation.actor.id}`, participation);
acc.set(day, participations);
return acc;
}, new Map());
@@ -273,7 +276,7 @@ export default class Home extends Vue {
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss">
<style lang="scss" scoped>
.search-autocomplete {
border: 1px solid #dbdbdb;
color: rgba(0, 0, 0, 0.87);