Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
<template>
<section class="container">
<h1>{{ $t("Event list") }}</h1>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline">
<EventCard
v-for="event in events"
:key="event.uuid"
:event="event"
class="column is-one-quarter-desktop is-half-mobile"
/>
</div>
<b-message
v-if-else="events.length === 0 && $apollo.loading === false"
type="is-danger"
>{{ $t("No events found") }}</b-message
>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import EventCard from "../../components/Event/EventCard.vue";
import RouteName from "../../router/name";
import { IEvent } from "../../types/event.model";
@Component({
components: {
EventCard,
},
metaInfo() {
return {
title: this.$t("Event list") as string,
};
},
})
export default class EventList extends Vue {
@Prop(String) location!: string;
events = [];
loading = true;
locationChip = false;
locationText = "";
viewEvent(event: IEvent): void {
this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="container section" v-if="group">
<div class="container mx-auto" v-if="group">
<breadcrumbs-nav
:links="[
{
@@ -8,14 +8,14 @@
text: displayName(group),
},
{
name: RouteName.EVENTS,
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
text: $t('Events'),
},
]"
/>
<section>
<h1 class="title" v-if="group">
<h1 class="" v-if="group">
{{
$t("{group}'s events", {
group: displayName(group),
@@ -29,23 +29,24 @@
)
}}
</p>
<router-link
<o-button
tag="router-link"
variant="primary"
v-if="isCurrentActorAGroupModerator"
:to="{
name: RouteName.CREATE_EVENT,
query: { actorId: group.id },
}"
class="button is-primary"
>{{ $t("+ Create an event") }}</router-link
>{{ $t("+ Create an event") }}</o-button
>
<b-loading :active.sync="$apollo.loading"></b-loading>
<o-loading v-model:active="groupLoading"></o-loading>
<section v-if="group">
<subtitle>
<h2 class="text-2xl">
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
</subtitle>
<b-switch class="mb-4" v-model="showPassedEvents">{{
</h2>
<o-switch class="mb-4" v-model="showPassedEvents">{{
$t("Past events")
}}</b-switch>
}}</o-switch>
<grouped-multi-event-minimalist-card
:events="group.organizedEvents.elements"
:isCurrentActorMember="isCurrentActorMember"
@@ -53,7 +54,7 @@
<empty-content
v-if="
group.organizedEvents.elements.length === 0 &&
$apollo.loading === false
groupLoading === false
"
icon="calendar"
:inline="true"
@@ -69,13 +70,13 @@
)
}}
</p>
<b-button type="is-text" tag="a" :href="group.url">
<o-button type="is-text" tag="a" :href="group.url">
{{ $t("View the group profile on the original instance") }}
</b-button>
</o-button>
</div>
</template>
</empty-content>
<b-pagination
<o-pagination
class="mt-4"
:total="group.organizedEvents.total"
v-model="page"
@@ -85,116 +86,93 @@
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</o-pagination>
</section>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
<script lang="ts" setup>
import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue";
import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { displayName, IGroup, usernameWithDomain } from "../../types/actor";
import { displayName, IPerson, usernameWithDomain } from "../../types/actor";
import { useQuery } from "@vue/apollo-composable";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { computed } from "vue";
import { useRoute } from "vue-router";
import {
booleanTransformer,
integerTransformer,
useRouteQuery,
} from "vue-use-route-query";
import { MemberRole } from "@/types/enums";
import { useHead } from "@vueuse/head";
import { useI18n } from "vue-i18n";
const EVENTS_PAGE_LIMIT = 10;
@Component({
apollo: {
memberships: {
query: PERSON_MEMBERSHIPS,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.currentActor.id,
};
},
update: (data) => data.person.memberships.elements,
skip() {
return !this.currentActor || !this.currentActor.id;
},
},
group: {
query: FETCH_GROUP_EVENTS,
variables() {
return {
name: this.$route.params.preferredUsername,
beforeDateTime: this.showPassedEvents ? new Date() : null,
afterDateTime: this.showPassedEvents ? null : new Date(),
organisedEventsPage: this.page,
organisedEventsLimit: EVENTS_PAGE_LIMIT,
};
},
update: (data) => data.group,
},
},
components: {
EmptyContent,
Subtitle,
GroupedMultiEventMinimalistCard,
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { group } = this;
return {
title: this.$t("{group} events", {
group: displayName(group),
}) as string,
};
},
})
export default class GroupEvents extends Vue {
group!: IGroup;
const { currentActor } = useCurrentActorClient();
memberships!: IMember[];
const { result: membershipsResult } = useQuery<{
person: Pick<IPerson, "memberships">;
}>(
PERSON_MEMBERSHIPS,
() => ({ id: currentActor.value?.id }),
() => ({ enabled: currentActor.value?.id !== undefined })
);
const memberships = computed(
() => membershipsResult.value?.person.memberships.elements
);
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
const route = useRoute();
const page = useRouteQuery("page", 1, integerTransformer);
const showPassedEvents = useRouteQuery(
"showPassedEvents",
false,
booleanTransformer
);
set page(page: number) {
this.$router.push({
name: RouteName.GROUP_EVENTS,
query: { ...this.$route.query, page: page.toString() },
});
this.$apollo.queries.group.refetch();
}
const { result: groupResult, loading: groupLoading } = useQuery(
FETCH_GROUP_EVENTS,
() => ({
name: route.params.preferredUsername,
beforeDateTime: showPassedEvents.value ? new Date() : null,
afterDateTime: showPassedEvents.value ? null : new Date(),
organisedEventsPage: page.value,
organisedEventsLimit: EVENTS_PAGE_LIMIT,
})
);
const group = computed(() => groupResult.value?.group);
usernameWithDomain = usernameWithDomain;
const { t } = useI18n({ useScope: "global" });
useHead({
title: t("{group} events", {
group: displayName(group.value),
}),
});
displayName = displayName;
const isCurrentActorMember = computed((): boolean => {
if (!group.value || !memberships.value) return false;
return (memberships.value ?? [])
.map(({ parent: { id } }) => id)
.includes(group.value.id);
});
RouteName = RouteName;
const isCurrentActorAGroupModerator = computed((): boolean => {
return hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
});
EVENTS_PAGE_LIMIT = EVENTS_PAGE_LIMIT;
get isCurrentActorMember(): boolean {
if (!this.group || !this.memberships) return false;
return this.memberships
.map(({ parent: { id } }) => id)
.includes(this.group.id);
}
get showPassedEvents(): boolean {
return this.$route.query.future === "false";
}
set showPassedEvents(value: boolean) {
this.$router.replace({ query: { future: (!value).toString() } });
}
}
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
return (
memberships.value !== undefined &&
memberships.value?.length > 0 &&
roles.includes(memberships.value[0].role)
);
};
</script>
<style lang="scss" scoped>
.container.section {
background: $white;
}
div.event-list {
margin-bottom: 1rem;
}
</style>

View File

@@ -1,78 +1,75 @@
<template>
<div class="section container">
<h1 class="title">
{{ $t("My events") }}
<div class="container mx-auto">
<h1 class="text-4xl">
{{ t("My events") }}
</h1>
<p>
{{
$t(
t(
"You will find here all the events you have created or of which you are a participant, as well as events organized by groups you follow or are a member of."
)
}}
</p>
<div class="buttons" v-if="!hideCreateEventButton">
<router-link
class="button is-primary"
<div class="my-2" v-if="!hideCreateEventButton">
<o-button
tag="router-link"
variant="primary"
:to="{ name: RouteName.CREATE_EVENT }"
>{{ $t("Create event") }}</router-link
>{{ t("Create event") }}</o-button
>
</div>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div class="wrapper">
<div class="event-filter">
<b-field grouped group-multiline>
<b-field>
<b-switch v-model="showUpcoming">{{
showUpcoming ? $t("Upcoming events") : $t("Past events")
}}</b-switch>
</b-field>
<b-field v-if="showUpcoming">
<b-checkbox v-model="showDrafts">{{ $t("Drafts") }}</b-checkbox>
</b-field>
<b-field v-if="showUpcoming">
<b-checkbox v-model="showAttending">{{
$t("Attending")
}}</b-checkbox>
</b-field>
<b-field v-if="showUpcoming">
<b-checkbox v-model="showMyGroups">{{
$t("From my groups")
}}</b-checkbox>
</b-field>
<p v-if="!showUpcoming">
{{
$tc(
"You have attended {count} events in the past.",
pastParticipations.total,
{
count: pastParticipations.total,
}
)
}}
</p>
<b-field
class="date-filter"
expanded
:label="
showUpcoming
? $t('Showing events starting on')
: $t('Showing events before')
"
>
<b-datepicker
v-model="dateFilter"
:first-day-of-week="firstDayOfWeek"
/>
<b-button
@click="dateFilter = new Date()"
class="reset-area"
icon-left="close"
:title="$t('Clear date filter field')"
/>
</b-field>
</b-field>
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
<div class="wrapper flex flex-wrap gap-4 items-start">
<div class="event-filter text-violet-1 flex-auto md:flex-none">
<o-field>
<o-switch v-model="showUpcoming">{{
showUpcoming ? t("Upcoming events") : t("Past events")
}}</o-switch>
</o-field>
<o-field v-if="showUpcoming">
<o-checkbox v-model="showDrafts">{{ t("Drafts") }}</o-checkbox>
</o-field>
<o-field v-if="showUpcoming">
<o-checkbox v-model="showAttending">{{ t("Attending") }}</o-checkbox>
</o-field>
<o-field v-if="showUpcoming">
<o-checkbox v-model="showMyGroups">{{
t("From my groups")
}}</o-checkbox>
</o-field>
<p v-if="!showUpcoming">
{{
t(
"You have attended {count} events in the past.",
{
count: pastParticipations.total,
},
pastParticipations.total
)
}}
</p>
<o-field
class="date-filter"
expanded
:label="
showUpcoming
? t('Showing events starting on')
: t('Showing events before')
"
>
<o-datepicker
v-model="dateFilter"
:first-day-of-week="firstDayOfWeek"
/>
<o-button
@click="dateFilter = new Date()"
class="reset-area"
icon-left="close"
:title="t('Clear date filter field')"
/>
</o-field>
</div>
<div class="my-events">
<div class="my-events flex-1">
<section
class="py-4"
v-if="showUpcoming && showDrafts && drafts.length > 0"
@@ -82,13 +79,15 @@
<section
class="py-4"
v-if="
showUpcoming && monthlyFutureEvents && monthlyFutureEvents.size > 0
showUpcoming &&
monthlyFutureEvents &&
monthlyFutureEvents.length > 0
"
>
<transition-group name="list" tag="p">
<div
class="mb-5"
v-for="month in monthlyFutureEvents"
v-for="month of monthlyFutureEvents()"
:key="month[0]"
>
<span class="upcoming-month">{{ month[0] }}</span>
@@ -102,7 +101,8 @@
/>
<event-minimalist-card
v-else-if="
!monthParticipationsIds(month[1]).includes(element.id)
element.id &&
!monthParticipationsIds(month[1]).includes(element?.id)
"
:event="element"
class="participation"
@@ -111,7 +111,7 @@
</div>
</transition-group>
<div class="columns is-centered">
<b-button
<o-button
class="column is-narrow"
v-if="
hasMoreFutureParticipations &&
@@ -119,9 +119,9 @@
futureParticipations.length === limit
"
@click="loadMoreFutureParticipations"
size="is-large"
type="is-primary"
>{{ $t("Load more") }}</b-button
size="large"
variant="primary"
>{{ t("Load more") }}</o-button
>
</div>
</section>
@@ -130,34 +130,34 @@
v-if="
showUpcoming &&
monthlyFutureEvents &&
monthlyFutureEvents.size === 0 &&
!$apollo.loading
monthlyFutureEvents.length === 0 &&
true // !$apollo.loading
"
>
<div class="img-container" :class="{ webp: supportsWebPFormat }" />
<div class="img-container h-64" />
<div class="content has-text-centered">
<p>
{{
$t(
t(
"You don't have any upcoming events. Maybe try another filter?"
)
}}
</p>
<i18n
path="Do you wish to {create_event} or {explore_events}?"
<i18n-t
keypath="Do you wish to {create_event} or {explore_events}?"
tag="p"
>
<router-link
:to="{ name: RouteName.CREATE_EVENT }"
slot="create_event"
>{{ $t("create an event") }}</router-link
>
<router-link
:to="{ name: RouteName.SEARCH }"
slot="explore_events"
>{{ $t("explore the events") }}</router-link
>
</i18n>
<template v-slot:create_event>
<router-link :to="{ name: RouteName.CREATE_EVENT }">{{
t("create an event")
}}</router-link>
</template>
<template v-slot:explore_events>
<router-link :to="{ name: RouteName.SEARCH }">{{
t("explore the events")
}}</router-link>
</template>
</i18n-t>
</div>
</section>
<section v-if="!showUpcoming && pastParticipations.elements.length > 0">
@@ -167,7 +167,7 @@
<event-participation-card
v-for="participation in month[1]"
:key="participation.id"
:participation="participation"
:participation="(participation as IParticipant)"
:options="{ hideDate: false }"
@event-deleted="eventDeleted"
class="participation"
@@ -175,16 +175,16 @@
</div>
</transition-group>
<div class="columns is-centered">
<b-button
<o-button
class="column is-narrow"
v-if="
hasMorePastParticipations &&
pastParticipations.elements.length === limit
"
@click="loadMorePastParticipations"
size="is-large"
type="is-primary"
>{{ $t("Load more") }}</b-button
size="large"
variant="primary"
>{{ t("Load more") }}</o-button
>
</div>
</section>
@@ -193,302 +193,230 @@
</div>
</template>
<script lang="ts">
import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import { Component, Vue } from "vue-property-decorator";
<script lang="ts" setup>
import { ParticipantRole } from "@/types/enums";
import RouteName from "@/router/name";
import { supportsWebPFormat } from "@/utils/support";
import { IParticipant, Participant } from "../../types/participant.model";
import { IParticipant } from "../../types/participant.model";
import { LOGGED_USER_DRAFTS } from "../../graphql/actor";
import { EventModel, IEvent } from "../../types/event.model";
import { IEvent } from "../../types/event.model";
import EventParticipationCard from "../../components/Event/EventParticipationCard.vue";
import MultiEventMinimalistCard from "../../components/Event/MultiEventMinimalistCard.vue";
import EventMinimalistCard from "../../components/Event/EventMinimalistCard.vue";
import Subtitle from "../../components/Utils/Subtitle.vue";
import {
LOGGED_USER_PARTICIPATIONS,
LOGGED_USER_UPCOMING_EVENTS,
} from "@/graphql/participant";
import { Paginate } from "@/types/paginate";
import { useQuery } from "@vue/apollo-composable";
import { computed, inject, ref } from "vue";
import { IUser } from "@/types/current-user.model";
import { booleanTransformer, useRouteQuery } from "vue-use-route-query";
import { Locale } from "date-fns";
import { useI18n } from "vue-i18n";
import { useRestrictions } from "@/composition/apollo/config";
type Eventable = IParticipant | IEvent;
@Component({
components: {
Subtitle,
MultiEventMinimalistCard,
EventParticipationCard,
EventMinimalistCard,
},
apollo: {
config: CONFIG,
userUpcomingEvents: {
query: LOGGED_USER_UPCOMING_EVENTS,
fetchPolicy: "cache-and-network",
variables() {
return {
page: 1,
limit: 10,
afterDateTime: this.dateFilter,
};
},
update(data) {
this.futureParticipations = data.loggedUser.participations.elements.map(
(participation: IParticipant) => new Participant(participation)
);
this.groupEvents = data.loggedUser.followedGroupEvents.elements.map(
({ event }: { event: IEvent }) => event
);
},
},
drafts: {
query: LOGGED_USER_DRAFTS,
fetchPolicy: "cache-and-network",
variables: {
page: 1,
limit: 10,
},
update: (data) =>
data.loggedUser.drafts.map((event: IEvent) => new EventModel(event)),
},
pastParticipations: {
query: LOGGED_USER_PARTICIPATIONS,
fetchPolicy: "cache-and-network",
variables() {
return {
page: 1,
limit: 10,
beforeDateTime: this.dateFilter,
};
},
update: (data) => data.loggedUser.participations,
},
},
metaInfo() {
return {
title: this.$t("My events") as string,
};
},
})
export default class MyEvents extends Vue {
futurePage = 1;
const { t } = useI18n({ useScope: "global" });
pastPage = 1;
const futurePage = ref(1);
const pastPage = ref(1);
const limit = ref(10);
limit = 10;
get showUpcoming(): boolean {
return ((this.$route.query.showUpcoming as string) || "true") === "true";
}
set showUpcoming(showUpcoming: boolean) {
this.$router.push({
name: RouteName.MY_EVENTS,
query: { ...this.$route.query, showUpcoming: showUpcoming.toString() },
});
}
get showDrafts(): boolean {
return ((this.$route.query.showDrafts as string) || "true") === "true";
}
set showDrafts(showDrafts: boolean) {
this.$router.push({
name: RouteName.MY_EVENTS,
query: { ...this.$route.query, showDrafts: showDrafts.toString() },
});
}
get showAttending(): boolean {
return ((this.$route.query.showAttending as string) || "true") === "true";
}
set showAttending(showAttending: boolean) {
this.$router.push({
name: RouteName.MY_EVENTS,
query: { ...this.$route.query, showAttending: showAttending.toString() },
});
}
get showMyGroups(): boolean {
return ((this.$route.query.showMyGroups as string) || "false") === "true";
}
set showMyGroups(showMyGroups: boolean) {
this.$router.push({
name: RouteName.MY_EVENTS,
query: { ...this.$route.query, showMyGroups: showMyGroups.toString() },
});
}
get dateFilter(): Date {
const query = this.$route.query.dateFilter as string;
const showUpcoming = useRouteQuery("showUpcoming", true, booleanTransformer);
const showDrafts = useRouteQuery("showDrafts", true, booleanTransformer);
const showAttending = useRouteQuery("showAttending", true, booleanTransformer);
const showMyGroups = useRouteQuery("showMyGroups", false, booleanTransformer);
const dateFilter = useRouteQuery("dateFilter", new Date(), {
fromQuery(query) {
if (query && /(\d{4}-\d{2}-\d{2})/.test(query)) {
return new Date(`${query}T00:00:00Z`);
}
return new Date();
}
set dateFilter(date: Date) {
},
toQuery(value: Date) {
const pad = (number: number) => {
if (number < 10) {
return "0" + number;
}
return number;
};
const stringifiedDate = `${date.getFullYear()}-${pad(
date.getMonth() + 1
)}-${pad(date.getDate())}`;
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(
value.getDate()
)}`;
},
});
if (this.$route.query.dateFilter !== stringifiedDate) {
this.$router.push({
name: RouteName.MY_EVENTS,
query: {
...this.$route.query,
dateFilter: stringifiedDate,
},
});
const hasMoreFutureParticipations = ref(true);
const hasMorePastParticipations = ref(true);
// config: CONFIG
const {
result: loggedUserUpcomingEventsResult,
fetchMore: fetchMoreUpcomingEvents,
} = useQuery<{
loggedUser: IUser;
}>(LOGGED_USER_UPCOMING_EVENTS, () => ({
page: 1,
limit: 10,
afterDateTime: dateFilter.value,
}));
const futureParticipations = computed(
() =>
loggedUserUpcomingEventsResult.value?.loggedUser.participations.elements ??
[]
);
const groupEvents = computed(
() =>
loggedUserUpcomingEventsResult.value?.loggedUser.followedGroupEvents
.elements ?? []
);
const { result: draftsResult } = useQuery<{
loggedUser: Pick<IUser, "drafts">;
}>(LOGGED_USER_DRAFTS, () => ({ page: 1, limit: 10 }));
const drafts = computed(() => draftsResult.value?.loggedUser.drafts ?? []);
const { result: participationsResult, fetchMore: fetchMoreParticipations } =
useQuery<{
loggedUser: Pick<IUser, "participations">;
}>(LOGGED_USER_PARTICIPATIONS, () => ({ page: 1, limit: 10 }));
const pastParticipations = computed(
() =>
participationsResult.value?.loggedUser.participations ?? {
elements: [],
total: 0,
}
}
);
config!: IConfig;
// metaInfo() {
// return {
// title: this.t("My events") as string,
// };
// },
futureParticipations: IParticipant[] = [];
groupEvents: IEvent[] = [];
hasMoreFutureParticipations = true;
pastParticipations: Paginate<IParticipant> = { elements: [], total: 0 };
hasMorePastParticipations = true;
drafts: IEvent[] = [];
RouteName = RouteName;
supportsWebPFormat = supportsWebPFormat;
static monthlyEvents(
elements: Eventable[],
revertSort = false
): Map<string, Eventable[]> {
const res = elements.filter((element: Eventable) => {
if ("role" in element) {
return (
element.event.beginsOn != null &&
element.role !== ParticipantRole.REJECTED
);
}
return element.beginsOn != null;
const monthlyEvents = (
elements: Eventable[],
revertSort = false
): Map<string, Eventable[]> => {
const res = elements.filter((element: Eventable) => {
if ("role" in element) {
return (
element.event.beginsOn != null &&
element.role !== ParticipantRole.REJECTED
);
}
return element.beginsOn != null;
});
if (revertSort) {
res.sort((a: Eventable, b: Eventable) => {
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
return new Date(bTime).getTime() - new Date(aTime).getTime();
});
if (revertSort) {
res.sort((a: Eventable, b: Eventable) => {
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
return new Date(bTime).getTime() - new Date(aTime).getTime();
});
} else {
res.sort((a: Eventable, b: Eventable) => {
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
return new Date(aTime).getTime() - new Date(bTime).getTime();
});
}
return res.reduce((acc: Map<string, Eventable[]>, element: Eventable) => {
const month = new Date(
"role" in element ? element.event.beginsOn : element.beginsOn
).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
});
const filteredElements: Eventable[] = acc.get(month) || [];
filteredElements.push(element);
acc.set(month, filteredElements);
return acc;
}, new Map());
}
get monthlyFutureEvents(): Map<string, Eventable[]> {
let eventable = [] as Eventable[];
if (this.showAttending) {
eventable = [...eventable, ...this.futureParticipations];
}
if (this.showMyGroups) {
eventable = [...eventable, ...this.groupEvents];
}
return MyEvents.monthlyEvents(eventable);
}
get monthlyPastParticipations(): Map<string, Eventable[]> {
return MyEvents.monthlyEvents(this.pastParticipations.elements, true);
}
monthParticipationsIds(elements: Eventable[]): string[] {
const res = elements.filter((element: Eventable) => {
return "role" in element;
}) as IParticipant[];
return res.map(({ event }: { event: IEvent }) => {
return event.id as string;
} else {
res.sort((a: Eventable, b: Eventable) => {
const aTime = "role" in a ? a.event.beginsOn : a.beginsOn;
const bTime = "role" in b ? b.event.beginsOn : b.beginsOn;
return new Date(aTime).getTime() - new Date(bTime).getTime();
});
}
return res.reduce((acc: Map<string, Eventable[]>, element: Eventable) => {
const month = new Date(
"role" in element ? element.event.beginsOn : element.beginsOn
).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
});
const filteredElements: Eventable[] = acc.get(month) || [];
filteredElements.push(element);
acc.set(month, filteredElements);
return acc;
}, new Map());
};
loadMoreFutureParticipations(): void {
this.futurePage += 1;
if (this.$apollo.queries.futureParticipations) {
this.$apollo.queries.futureParticipations.fetchMore({
// New variables
variables: {
page: this.futurePage,
limit: this.limit,
},
});
}
const monthlyFutureEvents = (): Map<string, Eventable[]> => {
let eventable = [] as Eventable[];
if (showAttending.value) {
eventable = [...eventable, ...futureParticipations.value];
}
loadMorePastParticipations(): void {
this.pastPage += 1;
if (this.$apollo.queries.pastParticipations) {
this.$apollo.queries.pastParticipations.fetchMore({
// New variables
variables: {
page: this.pastPage,
limit: this.limit,
},
});
}
if (showMyGroups.value) {
eventable = [...eventable, ...groupEvents.value];
}
return monthlyEvents(eventable);
};
eventDeleted(eventid: string): void {
this.futureParticipations = this.futureParticipations.filter(
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
return monthlyEvents(pastParticipations.value.elements, true);
});
const monthParticipationsIds = (elements: Eventable[]): string[] => {
const res = elements.filter((element: Eventable) => {
return "role" in element;
}) as IParticipant[];
return res.map(({ event }: { event: IEvent }) => {
return event.id as string;
});
};
const loadMoreFutureParticipations = (): void => {
futurePage.value += 1;
if (fetchMoreUpcomingEvents) {
fetchMoreUpcomingEvents({
// New variables
variables: {
page: futurePage.value,
limit: limit.value,
},
});
}
};
const loadMorePastParticipations = (): void => {
pastPage.value += 1;
if (fetchMoreParticipations) {
fetchMoreParticipations({
// New variables
variables: {
page: pastPage.value,
limit: limit.value,
},
});
}
};
const eventDeleted = (eventid: string): void => {
futureParticipations.value = futureParticipations.value.filter(
(participation) => participation.event.id !== eventid
);
pastParticipations.value = {
elements: pastParticipations.value.elements.filter(
(participation) => participation.event.id !== eventid
);
this.pastParticipations = {
elements: this.pastParticipations.elements.filter(
(participation) => participation.event.id !== eventid
),
total: this.pastParticipations.total - 1,
};
}
),
total: pastParticipations.value.total - 1,
};
};
get hideCreateEventButton(): boolean {
return !!this.config?.restrictions?.onlyGroupsCanCreateEvents;
}
const { restrictions } = useRestrictions();
get firstDayOfWeek(): number {
return this.$dateFnsLocale?.options?.weekStartsOn || 0;
}
}
const hideCreateEventButton = computed((): boolean => {
return restrictions.value?.onlyGroupsCanCreateEvents === true;
});
const dateFnsLocale = inject<Locale>("dateFnsLocale");
const firstDayOfWeek = computed((): number => {
return dateFnsLocale?.options?.weekStartsOn ?? 0;
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
@import "~bulma/sass/utilities/mixins.sass";
// @import "node_modules/bulma/sass/utilities/mixins.sass";
main > .container {
background: $white;
// background: $white;
& > h1 {
margin: 10px auto 5px;
@@ -524,24 +452,18 @@ section {
.not-found {
margin-top: 2rem;
.img-container {
background-image: url("../../../public/img/pics/event_creation-480w.jpg");
background-image: url("/img/pics/event_creation-480w.webp");
@media (min-resolution: 2dppx) {
& {
background-image: url("../../../public/img/pics/event_creation-1024w.jpg");
}
}
&.webp {
background-image: url("../../../public/img/pics/event_creation-480w.webp");
@media (min-resolution: 2dppx) {
& {
background-image: url("../../../public/img/pics/event_creation-1024w.webp");
}
background-image: url("/img/pics/event_creation-1024w.webp");
}
}
max-width: 450px;
height: 300px;
box-shadow: 0 0 8px 8px white inset;
@media (prefers-color-scheme: dark) {
box-shadow: 0 0 8px 8px #374151 inset;
}
background-size: cover;
border-radius: 10px;
margin: auto auto 1rem;
@@ -549,15 +471,15 @@ section {
}
.wrapper {
display: grid;
grid-template-areas: "filter" "events";
align-items: start;
// display: grid;
// grid-template-areas: "filter" "events";
// align-items: start;
@include desktop {
gap: 2rem;
grid-template-columns: 1fr 3fr;
grid-template-areas: "filter events";
}
// // @include desktop {
// gap: 2rem;
// grid-template-columns: 1fr 3fr;
// grid-template-areas: "filter events";
// // }
.event-filter {
grid-area: filter;
@@ -565,18 +487,18 @@ section {
border-radius: 5px;
padding: 0.75rem 1.25rem 0.25rem;
@include desktop {
padding: 2rem 1.25rem;
::v-deep .field.is-grouped {
display: block;
}
}
// @include desktop {
// padding: 2rem 1.25rem;
// :deep(.field.is-grouped) {
// display: block;
// }
// }
::v-deep .field > .field {
:deep(.field > .field) {
margin: 0 auto 1.25rem !important;
}
.date-filter ::v-deep .field-body {
.date-filter :deep(.field-body) {
display: block;
}
}

View File

@@ -1,72 +1,82 @@
<template>
<section class="section container" v-if="event">
<section class="container mx-auto" v-if="event">
<breadcrumbs-nav
:links="[
{ name: RouteName.MY_EVENTS, text: $t('My events') },
{ name: RouteName.MY_EVENTS, text: t('My events') },
{
name: RouteName.EVENT,
params: { uuid: event.uuid },
text: event.title,
},
{
name: RouteName.PARTICIPANTS,
name: RouteName.PARTICIPATIONS,
params: { uuid: event.uuid },
text: $t('Participants'),
text: t('Participants'),
},
]"
/>
<h1 class="title">{{ $t("Participants") }}</h1>
<div class="level">
<div class="level-left">
<div class="level-item">
<b-field :label="$t('Status')" horizontal label-for="role-select">
<b-select v-model="role" id="role-select">
<h1>{{ t("Participants") }}</h1>
<div class="">
<div class="">
<div class="">
<o-field :label="t('Status')" horizontal label-for="role-select">
<o-select v-model="role" id="role-select">
<option :value="null">
{{ $t("Everything") }}
{{ t("Everything") }}
</option>
<option :value="ParticipantRole.CREATOR">
{{ $t("Organizer") }}
{{ t("Organizer") }}
</option>
<option :value="ParticipantRole.PARTICIPANT">
{{ $t("Participant") }}
{{ t("Participant") }}
</option>
<option :value="ParticipantRole.NOT_APPROVED">
{{ $t("Not approved") }}
{{ t("Not approved") }}
</option>
<option :value="ParticipantRole.REJECTED">
{{ $t("Rejected") }}
{{ t("Rejected") }}
</option>
</b-select>
</b-field>
</o-select>
</o-field>
</div>
<div class="level-item" v-if="exportFormats.length > 0">
<b-dropdown aria-role="list">
<div class="" v-if="exportFormats.length > 0">
<o-dropdown aria-role="list">
<template #trigger="{ active }">
<b-button
:label="$t('Export')"
type="is-primary"
<o-button
:label="t('Export')"
variant="primary"
:icon-right="active ? 'menu-up' : 'menu-down'"
/>
</template>
<b-dropdown-item
<o-dropdown-item
has-link
v-for="format in exportFormats"
:key="format"
aria-role="listitem"
@click="exportParticipants(format)"
@keyup.enter="exportParticipants(format)"
@click="
exportParticipants({
eventId: event?.id,
format,
})
"
@keyup.enter="
exportParticipants({
eventId: event.value?.id,
format,
})
"
>
<button class="dropdown-button">
<b-icon :icon="formatToIcon(format)"></b-icon>
<o-icon :icon="formatToIcon(format)"></o-icon>
{{ format }}
</button>
</b-dropdown-item>
</b-dropdown>
</o-dropdown-item>
</o-dropdown>
</div>
</div>
</div>
<b-table
<o-table
:data="event.participants.elements"
ref="queueTable"
detailed
@@ -76,26 +86,26 @@
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
checkbox-position="left"
:show-detail-icon="false"
:loading="this.$apollo.loading"
:loading="participantsLoading"
paginated
:current-page="page"
backend-pagination
:pagination-simple="true"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
:total="event.participants.total"
:per-page="PARTICIPANTS_PER_PAGE"
backend-sorting
:default-sort-direction="'desc'"
:default-sort="['insertedAt', 'desc']"
@page-change="(newPage) => (page = newPage)"
@sort="(field, order) => $emit('sort', field, order)"
@sort="(field, order) => emit('sort', field, order)"
>
<b-table-column
<o-table-column
field="actor.preferredUsername"
:label="$t('Participant')"
:label="t('Participant')"
v-slot="props"
>
<article class="media">
@@ -105,20 +115,13 @@
>
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
</figure>
<b-icon
class="media-left"
<Incognito
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
size="is-large"
icon="incognito"
/>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
:size="48"
/>
<AccountCircle v-else :size="48" />
<div class="media-content">
<div class="content">
<div class="prose dark:prose-invert">
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
<span v-if="props.row.actor.name">{{
props.row.actor.name
@@ -129,42 +132,42 @@
>
</span>
<span v-else>
{{ $t("Anonymous participant") }}
{{ t("Anonymous participant") }}
</span>
</div>
</div>
</article>
</b-table-column>
<b-table-column field="role" :label="$t('Role')" v-slot="props">
</o-table-column>
<o-table-column field="role" :label="t('Role')" v-slot="props">
<b-tag
type="is-primary"
variant="primary"
v-if="props.row.role === ParticipantRole.CREATOR"
>
{{ $t("Organizer") }}
{{ t("Organizer") }}
</b-tag>
<b-tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
{{ $t("Participant") }}
{{ t("Participant") }}
</b-tag>
<b-tag v-else-if="props.row.role === ParticipantRole.NOT_CONFIRMED">
{{ $t("Not confirmed") }}
{{ t("Not confirmed") }}
</b-tag>
<b-tag
type="is-warning"
variant="warning"
v-else-if="props.row.role === ParticipantRole.NOT_APPROVED"
>
{{ $t("Not approved") }}
{{ t("Not approved") }}
</b-tag>
<b-tag
type="is-danger"
variant="danger"
v-else-if="props.row.role === ParticipantRole.REJECTED"
>
{{ $t("Rejected") }}
{{ t("Rejected") }}
</b-tag>
</b-table-column>
<b-table-column
</o-table-column>
<o-table-column
field="metadata.message"
class="column-message"
:label="$t('Message')"
:label="t('Message')"
v-slot="props"
>
<div
@@ -176,7 +179,7 @@
v-if="props.row.metadata && props.row.metadata.message"
>
<p v-if="props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH">
{{ props.row.metadata.message | ellipsize }}
{{ ellipsize(props.row.metadata.message) }}
</p>
<p v-else>
{{ props.row.metadata.message }}
@@ -188,67 +191,64 @@
@click.stop="toggleQueueDetails(props.row)"
>
{{
openDetailedRows[props.row.id] ? $t("View less") : $t("View more")
openDetailedRows[props.row.id] ? t("View less") : t("View more")
}}
</button>
</div>
<p v-else class="has-text-grey-dark">
{{ $t("No message") }}
{{ t("No message") }}
</p>
</b-table-column>
<b-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
<span class="has-text-centered">
{{ props.row.insertedAt | formatDateString }}<br />{{
props.row.insertedAt | formatTimeString
</o-table-column>
<o-table-column field="insertedAt" :label="t('Date')" v-slot="props">
<span class="text-center">
{{ formatDateString(props.row.insertedAt) }}<br />{{
formatTimeString(props.row.insertedAt)
}}
</span>
</b-table-column>
</o-table-column>
<template #detail="props">
<article v-html="nl2br(props.row.metadata.message)" />
</template>
<template slot="empty">
<section class="section">
<div class="content has-text-grey-dark has-text-centered">
<p>{{ $t("No participant matches the filters") }}</p>
</div>
</section>
<template #empty>
<EmptyContent icon="account-circle" :inline="true">
{{ t("No participant matches the filters") }}
</EmptyContent>
</template>
<template slot="bottom-left">
<div class="buttons">
<b-button
<template #bottom-left>
<div class="flex gap-2">
<o-button
@click="acceptParticipants(checkedRows)"
type="is-success"
variant="success"
:disabled="!canAcceptParticipants"
>
{{
$tc(
t(
"No participant to approve|Approve participant|Approve {number} participants",
checkedRows.length,
{ number: checkedRows.length }
{ number: checkedRows.length },
checkedRows.length
)
}}
</b-button>
<b-button
</o-button>
<o-button
@click="refuseParticipants(checkedRows)"
type="is-danger"
variant="danger"
:disabled="!canRefuseParticipants"
>
{{
$tc(
t(
"No participant to reject|Reject participant|Reject {number} participants",
checkedRows.length,
{ number: checkedRows.length }
{ number: checkedRows.length },
checkedRows.length
)
}}
</b-button>
</o-button>
</div>
</template>
</b-table>
</o-table>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
<script lang="ts" setup>
import { ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEvent, IEventParticipantStats } from "../../types/event.model";
@@ -257,263 +257,206 @@ import {
PARTICIPANTS,
UPDATE_PARTICIPANT,
} from "../../graphql/event";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor";
import { EVENT_PARTICIPANTS } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import { usernameWithDomain } from "../../types/actor";
import { nl2br } from "../../utils/html";
import { asyncForEach } from "../../utils/asyncForEach";
import RouteName from "../../router/name";
import VueRouter from "vue-router";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useParticipantsExportFormats } from "@/composition/config";
import { useMutation, useQuery } from "@vue/apollo-composable";
import {
integerTransformer,
enumTransformer,
useRouteQuery,
} from "vue-use-route-query";
import { computed, inject, ref } from "vue";
import { formatDateString, formatTimeString } from "@/filters/datetime";
import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Incognito from "vue-material-design-icons/Incognito.vue";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { Notifier } from "@/plugins/notifier";
const PARTICIPANTS_PER_PAGE = 10;
const MESSAGE_ELLIPSIS_LENGTH = 130;
type exportFormat = "CSV" | "PDF" | "ODS";
@Component({
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
config: EVENT_PARTICIPANTS,
event: {
query: PARTICIPANTS,
variables() {
return {
uuid: this.eventId,
page: this.page,
limit: PARTICIPANTS_PER_PAGE,
roles: this.role,
};
},
skip() {
return !this.currentActor.id;
},
},
},
filters: {
ellipsize: (text?: string) =>
text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat(""),
},
metaInfo() {
return {
title: this.$t("Participants") as string,
};
},
})
export default class Participants extends Vue {
@Prop({ required: true }) eventId!: string;
const props = defineProps<{
eventId: string;
}>();
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
const { t } = useI18n({ useScope: "global" });
set page(page: number) {
this.pushRouter(RouteName.PARTICIPATIONS, {
page: page.toString(),
const { currentActor } = useCurrentActorClient();
const participantsExportFormats = useParticipantsExportFormats();
const ellipsize = (text?: string) =>
text && text.substring(0, MESSAGE_ELLIPSIS_LENGTH).concat("");
// metaInfo() {
// return {
// title: this.t("Participants") as string,
// };
// },
const page = useRouteQuery("page", 1, integerTransformer);
const role = useRouteQuery(
"role",
ParticipantRole.PARTICIPANT,
enumTransformer(ParticipantRole)
);
const limit = ref(PARTICIPANTS_PER_PAGE);
const checkedRows = ref<IParticipant[]>([]);
// const queueTable = ref(null);
const { result: participantsResult, loading: participantsLoading } = useQuery<{
event: IEvent;
}>(
PARTICIPANTS,
() => ({
uuid: props.eventId,
page: page.value,
limit: PARTICIPANTS_PER_PAGE,
roles: role.value,
}),
() => ({
enabled:
currentActor.value?.id !== undefined &&
page.value !== undefined &&
role.value !== undefined,
})
);
const event = computed(() => participantsResult.value?.event);
const participantStats = computed((): IEventParticipantStats | null => {
if (!event.value) return null;
return event.value.participantStats;
});
const { mutate: updateParticipant, onError: onUpdateParticipantError } =
useMutation(UPDATE_PARTICIPANT);
onUpdateParticipantError((e) => console.error(e));
const acceptParticipants = async (
participants: IParticipant[]
): Promise<void> => {
await asyncForEach(participants, async (participant: IParticipant) => {
await updateParticipant({
id: participant.id,
role: ParticipantRole.PARTICIPANT,
});
}
});
checkedRows.value = [];
};
get role(): ParticipantRole | null {
if (
Object.values(ParticipantRole).includes(
this.$route.query.role as ParticipantRole
)
) {
return this.$route.query.role as ParticipantRole;
}
return null;
}
set role(role: ParticipantRole | null) {
this.pushRouter(RouteName.PARTICIPATIONS, {
role: role || "",
const refuseParticipants = async (
participants: IParticipant[]
): Promise<void> => {
await asyncForEach(participants, async (participant: IParticipant) => {
await updateParticipant({
id: participant.id,
role: ParticipantRole.REJECTED,
});
});
checkedRows.value = [];
};
const {
mutate: exportParticipants,
onDone: onExportParticipantsMutationDone,
onError: onExportParticipantsMutationError,
} = useMutation(EXPORT_EVENT_PARTICIPATIONS);
onExportParticipantsMutationDone(({ data }) => {
const link =
window.origin +
"/exports/" +
type.toLowerCase() +
"/" +
exportEventParticipants;
console.log(link);
const a = document.createElement("a");
a.style.display = "none";
document.body.appendChild(a);
a.href = link;
a.setAttribute("download", "true");
a.click();
window.URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
const notifier = inject<Notifier>("notifier");
onExportParticipantsMutationError((e) => {
console.error(e);
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
notifier?.error(e.graphQLErrors[0].message);
}
});
limit = PARTICIPANTS_PER_PAGE;
const exportFormats = computed((): exportFormat[] => {
return (participantsExportFormats ?? []).map(
(key) => key.toUpperCase() as exportFormat
);
});
event!: IEvent;
config!: IConfig;
ParticipantRole = ParticipantRole;
currentActor!: IPerson;
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
checkedRows: IParticipant[] = [];
RouteName = RouteName;
usernameWithDomain = usernameWithDomain;
@Ref("queueTable") readonly queueTable!: any;
get participantStats(): IEventParticipantStats | null {
if (!this.event) return null;
return this.event.participantStats;
const formatToIcon = (format: exportFormat): string => {
switch (format) {
case "CSV":
return "file-delimited";
case "PDF":
return "file-pdf-box";
case "ODS":
return "google-spreadsheet";
}
};
async acceptParticipant(participant: IParticipant): Promise<void> {
try {
await this.$apollo.mutate({
mutation: UPDATE_PARTICIPANT,
variables: {
id: participant.id,
role: ParticipantRole.PARTICIPANT,
},
});
} catch (e) {
console.error(e);
}
}
async refuseParticipant(participant: IParticipant): Promise<void> {
try {
await this.$apollo.mutate({
mutation: UPDATE_PARTICIPANT,
variables: {
id: participant.id,
role: ParticipantRole.REJECTED,
},
});
} catch (e) {
console.error(e);
}
}
async acceptParticipants(participants: IParticipant[]): Promise<void> {
await asyncForEach(participants, async (participant: IParticipant) => {
await this.acceptParticipant(participant);
});
this.checkedRows = [];
}
async refuseParticipants(participants: IParticipant[]): Promise<void> {
await asyncForEach(participants, async (participant: IParticipant) => {
await this.refuseParticipant(participant);
});
this.checkedRows = [];
}
async exportParticipants(type: exportFormat): Promise<void> {
try {
const {
data: { exportEventParticipants },
} = await this.$apollo.mutate({
mutation: EXPORT_EVENT_PARTICIPATIONS,
variables: {
eventId: this.event.id,
format: type,
},
});
const link =
window.origin +
"/exports/" +
type.toLowerCase() +
"/" +
exportEventParticipants;
console.log(link);
const a = document.createElement("a");
a.style.display = "none";
document.body.appendChild(a);
a.href = link;
a.setAttribute("download", "true");
a.click();
window.URL.revokeObjectURL(a.href);
document.body.removeChild(a);
} catch (e: any) {
console.error(e);
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
this.$notifier.error(e.graphQLErrors[0].message);
}
}
}
get exportFormats(): string[] {
return (this.config?.exportFormats?.eventParticipants || []).map((key) =>
key.toUpperCase()
);
}
formatToIcon(format: exportFormat): string {
switch (format) {
case "CSV":
return "file-delimited";
case "PDF":
return "file-pdf-box";
case "ODS":
return "google-spreadsheet";
}
}
/**
* We can accept participants if at least one of them is not approved
*/
get canAcceptParticipants(): boolean {
return this.checkedRows.some((participant: IParticipant) =>
[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(
participant.role
)
);
}
/**
* We can refuse participants if at least one of them is something different than not approved
*/
get canRefuseParticipants(): boolean {
return this.checkedRows.some(
(participant: IParticipant) =>
participant.role !== ParticipantRole.REJECTED
);
}
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
nl2br = nl2br;
toggleQueueDetails(row: IParticipant): void {
if (
row.metadata.message &&
row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH
/**
* We can accept participants if at least one of them is not approved
*/
const canAcceptParticipants = (): boolean => {
return checkedRows.value.some((participant: IParticipant) =>
[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(
participant.role
)
return;
this.queueTable.toggleDetails(row);
if (row.id) {
this.openDetailedRows[row.id] = !this.openDetailedRows[row.id];
}
}
);
};
openDetailedRows: Record<string, boolean> = {};
/**
* We can refuse participants if at least one of them is something different than not approved
*/
const canRefuseParticipants = (): boolean => {
return checkedRows.value.some(
(participant: IParticipant) => participant.role !== ParticipantRole.REJECTED
);
};
async pushRouter(
routeName: string,
args: Record<string, string>
): Promise<void> {
try {
await this.$router.push({
name: routeName,
query: { ...this.$route.query, ...args },
});
this.$apollo.queries.event.refetch();
} catch (e) {
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
throw Error(e.toString());
}
}
const toggleQueueDetails = (row: IParticipant): void => {
if (
row.metadata.message &&
row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH
)
return;
queueTable.value.toggleDetails(row);
if (row.id) {
openDetailedRows.value[row.id] = !openDetailedRows.value[row.id];
}
}
};
const openDetailedRows = <Record<string, boolean>>{};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
section.container.container {
padding: 1rem;
background: $white;
// background: $white;
}
.table {