build: switch from yarn to npm to manage js dependencies and move js contents to root

yarn v1 is being deprecated and starts to have some issues

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

1360
src/views/Event/EditView.vue Normal file

File diff suppressed because it is too large Load Diff

627
src/views/Event/EventView.vue Executable file
View File

@@ -0,0 +1,627 @@
<template>
<div class="container mx-auto">
<o-loading v-model:active="eventLoading" />
<div class="flex flex-col mb-3">
<event-banner :picture="event?.picture" />
<div
class="flex flex-col relative pb-2 bg-white dark:bg-zinc-700 my-4 rounded"
>
<div class="date-calendar-icon-wrapper relative" v-if="event?.beginsOn">
<skeleton-date-calendar-icon
v-if="eventLoading"
class="absolute left-3 -top-16"
/>
<date-calendar-icon
v-else
:date="event.beginsOn.toString()"
class="absolute left-3 -top-16"
/>
</div>
<section class="intro px-2 pt-4" dir="auto">
<div class="flex flex-wrap gap-2 justify-end">
<div class="flex-1 min-w-[300px]">
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-12 bg-slate-200 w-3/4"
/>
<h1
v-else
class="text-4xl font-bold m-0"
dir="auto"
:lang="event?.language"
>
{{ event?.title }}
</h1>
<div class="organizer">
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-64"
/>
<div v-else-if="event?.organizerActor && !event?.attributedTo">
<popover-actor-card
:actor="event.organizerActor"
:inline="true"
>
<i18n-t
keypath="By {username}"
dir="auto"
class="block truncate max-w-xs md:max-w-sm"
>
<template #username>
<span dir="ltr">{{
displayName(event.organizerActor)
}}</span>
</template>
</i18n-t>
</popover-actor-card>
</div>
<span v-else-if="event?.attributedTo">
<popover-actor-card
:actor="event.attributedTo"
:inline="true"
>
<i18n-t
keypath="By {group}"
dir="auto"
class="block truncate max-w-xs md:max-w-sm"
>
<template #group>
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(
event.attributedTo
),
},
}"
dir="ltr"
>{{ displayName(event.attributedTo) }}</router-link
>
</template>
</i18n-t>
</popover-actor-card>
</span>
</div>
<div class="flex flex-wrap items-center gap-2 gap-y-4 mt-2 my-3">
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-64"
/>
<p v-else-if="event?.status !== EventStatus.CONFIRMED">
<tag
variant="warning"
v-if="event?.status === EventStatus.TENTATIVE"
>{{ t("Event to be confirmed") }}</tag
>
<tag
variant="danger"
v-if="event?.status === EventStatus.CANCELLED"
>{{ t("Event cancelled") }}</tag
>
</p>
<template v-if="!eventLoading && !event?.draft">
<p
v-if="event?.visibility === EventVisibility.PUBLIC"
class="inline-flex gap-1"
>
<Earth />
{{ t("Public event") }}
</p>
<p
v-if="event?.visibility === EventVisibility.UNLISTED"
class="inline-flex gap-1"
>
<Link />
{{ t("Private event") }}
</p>
</template>
<template v-if="!event?.local && organizerDomain">
<a :href="event?.url">
<tag variant="info">{{ organizerDomain }}</tag>
</a>
</template>
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-64"
/>
<p v-else class="flex flex-wrap gap-1 items-center" dir="auto">
<tag v-if="eventCategory" class="category" capitalize>{{
eventCategory
}}</tag>
<router-link
class="rounded-md truncate text-sm text-violet-title py-1 bg-purple-3 dark:text-violet-3 category"
v-for="tag in event?.tags ?? []"
:key="tag.title"
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
>
<tag>{{ tag.title }}</tag>
</router-link>
</p>
<tag variant="warning" size="medium" v-if="event?.draft"
>{{ t("Draft") }}
</tag>
</div>
</div>
<div v-if="eventLoading">
<div class="animate-pulse mb-2 h-6 bg-slate-200 w-64" />
<div class="animate-pulse mb-2 h-6 bg-slate-200 w-64" />
</div>
<EventActionSection
v-else-if="event"
:event="event"
:currentActor="currentActor"
:participations="participations"
:person="person"
/>
</div>
</section>
</div>
<div
class="rounded-lg dark:border-violet-title flex flex-wrap flex-col md:flex-row-reverse gap-4"
>
<aside
class="rounded bg-white dark:bg-zinc-700 shadow-md h-min max-w-screen-sm"
>
<div class="sticky p-4">
<aside
v-if="eventLoading"
class="animate-pulse rounded bg-white dark:bg-zinc-700 h-min max-w-screen-sm"
>
<div class="mb-6 p-2" v-for="i in 3" :key="i">
<div class="mb-2 h-6 bg-slate-200 w-64" />
<div class="flex space-x-4 flex-row">
<div class="rounded-full bg-slate-200 h-10 w-10"></div>
<div class="flex flex-col flex-1 space-y-2">
<div class="h-3 bg-slate-200"></div>
<div class="h-3 bg-slate-200"></div>
</div>
</div>
</div>
</aside>
<event-metadata-sidebar
v-else-if="event"
:event="event"
:user="loggedUser"
@showMapModal="showMap = true"
/>
</div>
</aside>
<div class="flex-1">
<section
class="event-description bg-white dark:bg-zinc-700 px-3 pt-1 pb-3 rounded mb-4"
>
<h2 class="text-2xl">{{ t("About this event") }}</h2>
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-3/4"
/>
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-3/4"
/>
<div
v-if="eventLoading"
class="animate-pulse mb-2 h-6 space-y-6 bg-slate-200 w-1/4"
/>
<p v-else-if="!event?.description">
{{ t("The event organizer didn't add any description.") }}
</p>
<div v-else>
<div
:lang="event?.language"
dir="auto"
class="mt-4 prose md:prose-lg lg:prose-xl dark:prose-invert prose-h1:text-xl prose-h1:font-semibold prose-h2:text-lg prose-h3:text-base md:prose-h1:text-2xl md:prose-h1:font-semibold md:prose-h2:text-xl md:prose-h3:text-lg lg:prose-h1:text-2xl lg:prose-h1:font-semibold lg:prose-h2:text-xl lg:prose-h3:text-lg"
ref="eventDescriptionElement"
v-html="event.description"
/>
</div>
</section>
<section class="my-4">
<component
v-for="(metadata, integration) in integrations"
:is="metadataToComponent[integration]"
:key="integration"
:metadata="metadata"
class="my-2"
/>
</section>
<section
class="bg-white dark:bg-zinc-700 px-3 pt-1 pb-3 rounded my-4"
ref="commentsObserver"
>
<a href="#comments">
<h2 class="text-2xl" id="comments">{{ t("Comments") }}</h2>
</a>
<comment-tree v-if="event && loadComments" :event="event" />
</section>
</div>
</div>
<section
class="bg-white dark:bg-zinc-700 px-3 pt-1 pb-3 rounded my-4"
v-if="(event?.relatedEvents ?? []).length > 0"
>
<h2 class="text-2xl mb-2">
{{ t("These events may interest you") }}
</h2>
<multi-card :events="event?.relatedEvents ?? []" />
</section>
<o-modal
v-model:active="showMap"
:close-button-aria-label="t('Close')"
class="map-modal"
v-if="event?.physicalAddress?.geom"
has-modal-card
full-screen
:can-cancel="['escape', 'outside']"
>
<template #default>
<event-map
:routingType="routingType ?? RoutingType.OPENSTREETMAP"
:address="event.physicalAddress"
@close="showMap = false"
/>
</template>
</o-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import {
EventStatus,
ParticipantRole,
RoutingType,
EventVisibility,
} from "@/types/enums";
import {
EVENT_PERSON_PARTICIPATION,
// EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
} from "@/graphql/event";
import {
displayName,
IActor,
IPerson,
usernameWithDomain,
} from "@/types/actor";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import SkeletonDateCalendarIcon from "@/components/Event/SkeletonDateCalendarIcon.vue";
import Earth from "vue-material-design-icons/Earth.vue";
import Link from "vue-material-design-icons/Link.vue";
import MultiCard from "@/components/Event/MultiCard.vue";
import RouteName from "@/router/name";
import CommentTree from "@/components/Comment/CommentTree.vue";
import "intersection-observer";
import Tag from "@/components/TagElement.vue";
import EventMetadataSidebar from "@/components/Event/EventMetadataSidebar.vue";
import EventBanner from "@/components/Event/EventBanner.vue";
import EventActionSection from "@/components/Event/EventActionSection.vue";
import PopoverActorCard from "@/components/Account/PopoverActorCard.vue";
import { IEventMetadataDescription } from "@/types/event-metadata";
import { eventMetaDataList } from "@/services/EventMetadata";
import { useFetchEvent } from "@/composition/apollo/event";
import {
computed,
onMounted,
ref,
watch,
defineAsyncComponent,
inject,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import {
useCurrentActorClient,
usePersonStatusGroup,
} from "@/composition/apollo/actor";
import { useLoggedUser } from "@/composition/apollo/user";
import { useQuery } from "@vue/apollo-composable";
import {
useEventCategories,
useRoutingType,
} from "@/composition/apollo/config";
import { useI18n } from "vue-i18n";
import { Notifier } from "@/plugins/notifier";
import { AbsintheGraphQLErrors } from "@/types/errors.model";
import { useHead } from "@vueuse/head";
const IntegrationTwitch = defineAsyncComponent(
() => import("@/components/Event/Integrations/TwitchIntegration.vue")
);
const IntegrationPeertube = defineAsyncComponent(
() => import("@/components/Event/Integrations/PeerTubeIntegration.vue")
);
const IntegrationYoutube = defineAsyncComponent(
() => import("@/components/Event/Integrations/YouTubeIntegration.vue")
);
const IntegrationJitsiMeet = defineAsyncComponent(
() => import("@/components/Event/Integrations/JitsiMeetIntegration.vue")
);
const IntegrationEtherpad = defineAsyncComponent(
() => import("@/components/Event/Integrations/EtherpadIntegration.vue")
);
const EventMap = defineAsyncComponent(
() => import("@/components/Event/EventMap.vue")
);
const props = defineProps<{
uuid: string;
}>();
const { t } = useI18n({ useScope: "global" });
const propsUUID = computed(() => props.uuid);
const {
event,
onError: onFetchEventError,
loading: eventLoading,
refetch: refetchEvent,
} = useFetchEvent(propsUUID);
watch(propsUUID, (newUUid) => {
refetchEvent({ uuid: newUUid });
});
const eventId = computed(() => event.value?.id);
const { currentActor } = useCurrentActorClient();
const currentActorId = computed(() => currentActor.value?.id);
const { loggedUser } = useLoggedUser();
const {
result: participationsResult,
// subscribeToMore: subscribeToMoreParticipation,
} = useQuery<{ person: IPerson }>(
EVENT_PERSON_PARTICIPATION,
() => ({
eventId: event.value?.id,
actorId: currentActorId.value,
}),
() => ({
enabled:
currentActorId.value !== undefined &&
currentActorId.value !== null &&
eventId.value !== undefined,
})
);
// subscribeToMoreParticipation(() => ({
// document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
// variables: {
// eventId: eventId,
// actorId: currentActorId,
// },
// }));
const participations = computed(
() => participationsResult.value?.person.participations?.elements ?? []
);
const groupFederatedUsername = computed(() =>
usernameWithDomain(event.value?.attributedTo)
);
const { person } = usePersonStatusGroup(groupFederatedUsername);
const { eventCategories } = useEventCategories();
// metaInfo() {
// return {
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// title: this.eventTitle,
// meta: [
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// { name: "description", content: this.eventDescription },
// ],
// };
// },
const identity = ref<IPerson | undefined | null>(null);
const oldParticipationRole = ref<string | undefined>(undefined);
const observer = ref<IntersectionObserver | null>(null);
const commentsObserver = ref<Element | null>(null);
const loadComments = ref(false);
const eventTitle = computed((): undefined | string => {
return event.value?.title;
});
const eventDescription = computed((): undefined | string => {
return event.value?.description;
});
const route = useRoute();
const router = useRouter();
const eventDescriptionElement = ref<HTMLElement | null>(null);
onMounted(async () => {
identity.value = currentActor.value;
if (route.hash.includes("#comment-")) {
loadComments.value = true;
}
observer.value = new IntersectionObserver(
(entries) => {
// eslint-disable-next-line no-restricted-syntax
for (const entry of entries) {
if (entry) {
loadComments.value = entry.isIntersecting || loadComments.value;
}
}
},
{
rootMargin: "-50px 0px -50px",
}
);
if (commentsObserver.value) {
observer.value.observe(commentsObserver.value);
}
watch(eventDescription, () => {
if (!eventDescription.value) return;
if (!eventDescriptionElement.value) return;
eventDescriptionElement.value.addEventListener("click", ($event) => {
// TODO: Find the right type for target
let { target }: { target: any } = $event;
while (target && target.tagName !== "A") target = target.parentNode;
// handle only links that occur inside the component and do not reference external resources
if (target && target.matches(".hashtag") && target.href) {
// some sanity checks taken from vue-router:
// https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L106
const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } =
$event;
// don't handle with control keys
if (metaKey || altKey || ctrlKey || shiftKey) return;
// don't handle when preventDefault called
if (defaultPrevented) return;
// don't handle right clicks
if (button !== undefined && button !== 0) return;
// don't handle if `target="_blank"`
if (target && target.getAttribute) {
const linkTarget = target.getAttribute("target");
if (/\b_blank\b/i.test(linkTarget)) return;
}
// don't handle same page links/anchors
const url = new URL(target.href);
const to = url.pathname;
if (window.location.pathname !== to && $event.preventDefault) {
$event.preventDefault();
router.push(to);
}
}
});
});
// this.$on("event-deleted", () => {
// return router.push({ name: RouteName.HOME });
// });
});
const notifier = inject<Notifier>("notifier");
watch(participations, () => {
if (participations.value.length > 0) {
if (
oldParticipationRole.value &&
participations.value[0].role !== ParticipantRole.NOT_APPROVED &&
oldParticipationRole.value !== participations.value[0].role
) {
switch (participations.value[0].role) {
case ParticipantRole.PARTICIPANT:
participationConfirmedMessage();
break;
case ParticipantRole.REJECTED:
participationRejectedMessage();
break;
default:
participationChangedMessage();
break;
}
}
oldParticipationRole.value = participations.value[0].role;
}
});
const participationConfirmedMessage = () => {
notifier?.success(t("Your participation has been confirmed"));
};
const participationRejectedMessage = () => {
notifier?.error(t("Your participation has been rejected"));
};
const participationChangedMessage = () => {
notifier?.info(t("Your participation status has been changed"));
};
const handleErrors = (errors: AbsintheGraphQLErrors): void => {
if (
errors.some((error) => error.status_code === 404) ||
errors.some(({ message }) => message.includes("has invalid value $uuid"))
) {
router.replace({ name: RouteName.PAGE_NOT_FOUND });
}
};
onFetchEventError(({ graphQLErrors }) =>
handleErrors(graphQLErrors as AbsintheGraphQLErrors)
);
const metadataToComponent: Record<string, any> = {
"mz:live:twitch:url": IntegrationTwitch,
"mz:live:peertube:url": IntegrationPeertube,
"mz:live:youtube:url": IntegrationYoutube,
"mz:visio:jitsi_meet": IntegrationJitsiMeet,
"mz:notes:etherpad:url": IntegrationEtherpad,
};
const integrations = computed((): Record<string, IEventMetadataDescription> => {
return (event.value?.metadata ?? [])
.map((val) => {
const def = eventMetaDataList.find((dat) => dat.key === val.key);
return {
...def,
...val,
};
})
.reduce((acc: Record<string, IEventMetadataDescription>, metadata) => {
const component = metadataToComponent[metadata.key];
if (component !== undefined) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
acc[metadata.key] = metadata;
}
return acc;
}, {});
});
const showMap = ref(false);
const { routingType } = useRoutingType();
const eventCategory = computed((): string | undefined => {
if (event.value?.category === "MEETING") {
return undefined;
}
return (eventCategories.value ?? []).find((eventCategoryToFind) => {
return eventCategoryToFind.id === event.value?.category;
})?.label as string;
});
const organizer = computed((): IActor | null => {
if (event.value?.attributedTo?.id) {
return event.value.attributedTo;
}
if (event.value?.organizerActor) {
return event.value.organizerActor;
}
return null;
});
const organizerDomain = computed((): string | undefined => {
return organizer.value?.domain ?? undefined;
});
useHead({
title: computed(() => eventTitle.value ?? ""),
meta: [{ name: "description", content: eventDescription.value }],
});
</script>
<style>
.event-description a {
@apply inline-block p-1 bg-mbz-yellow-alt-200 text-black;
}
.event-description .mention.h-card {
@apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1;
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<div class="container mx-auto" v-if="group">
<breadcrumbs-nav
:links="[
{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) },
text: displayName(group),
},
{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
text: t('Events'),
},
]"
/>
<section>
<h1 class="" v-if="group">
{{
t("{group}'s events", {
group: displayName(group),
})
}}
</h1>
<p v-if="isCurrentActorMember">
{{
t(
"When a moderator from the group creates an event and attributes it to the group, it will show up here."
)
}}
</p>
<o-button
tag="router-link"
variant="primary"
v-if="isCurrentActorAGroupModerator"
:to="{
name: RouteName.CREATE_EVENT,
query: { actorId: group.id },
}"
>{{ t("+ Create an event") }}</o-button
>
<o-loading v-model:active="groupLoading"></o-loading>
<section v-if="group">
<h2 class="text-2xl">
{{ showPassedEvents ? t("Past events") : t("Upcoming events") }}
</h2>
<o-switch class="mb-4" v-model="showPassedEvents">{{
t("Past events")
}}</o-switch>
<grouped-multi-event-minimalist-card
class="mb-6"
:events="group.organizedEvents.elements"
:isCurrentActorMember="isCurrentActorMember"
:order="showPassedEvents ? 'DESC' : 'ASC'"
/>
<empty-content
v-if="
group.organizedEvents.elements.length === 0 &&
groupLoading === false
"
icon="calendar"
:inline="true"
:center="true"
>
{{ t("No events found") }}
<template v-if="group.domain !== null">
<div class="mt-4">
<p>
{{
t(
"This group is a remote group, it's possible the original instance has more informations."
)
}}
</p>
<o-button variant="text" tag="a" :href="group.url">
{{ t("View the group profile on the original instance") }}
</o-button>
</div>
</template>
</empty-content>
<o-pagination
v-if="group.organizedEvents.total > EVENTS_PAGE_LIMIT"
class="mt-4"
:total="group.organizedEvents.total"
v-model:current="page"
:per-page="EVENTS_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</section>
</section>
</div>
</template>
<script lang="ts" setup>
import RouteName from "@/router/name";
import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import {
displayName,
IGroup,
IPerson,
usernameWithDomain,
} from "../../types/actor";
import { useQuery } from "@vue/apollo-composable";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { computed, watch } 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;
const { currentActor } = useCurrentActorClient();
const { result: membershipsResult } = useQuery<{
person: Pick<IPerson, "memberships">;
}>(
PERSON_MEMBERSHIPS,
() => ({ id: currentActor.value?.id }),
() => ({
enabled:
currentActor.value?.id !== undefined && currentActor.value?.id !== null,
})
);
const memberships = computed(
() => membershipsResult.value?.person.memberships?.elements
);
const route = useRoute();
const page = useRouteQuery("page", 1, integerTransformer);
const showPassedEvents = useRouteQuery(
"showPassedEvents",
false,
booleanTransformer
);
/**
* Why is the following hack needed? Page doesn't want to be reactive!
* TODO: investigate
*/
const variables = computed(() => ({
name: route.params.preferredUsername as string,
beforeDateTime: showPassedEvents.value ? new Date() : null,
afterDateTime: showPassedEvents.value ? null : new Date(),
order: "BEGINS_ON",
orderDirection: showPassedEvents.value ? "DESC" : "ASC",
organisedEventsPage: page.value,
organisedEventsLimit: EVENTS_PAGE_LIMIT,
}));
watch(
variables,
(newVariables) => {
refetch(newVariables);
},
{ deep: true }
);
const {
result: groupResult,
loading: groupLoading,
refetch: refetch,
} = useQuery<
{
group: IGroup;
},
{
name: string;
beforeDateTime: Date | null;
afterDateTime: Date | null;
organisedEventsPage: number;
organisedEventsLimit: number;
}
>(FETCH_GROUP_EVENTS, variables);
const group = computed(() => groupResult.value?.group);
const { t } = useI18n({ useScope: "global" });
useHead({
title: () =>
t("{group} events", {
group: displayName(group.value),
}),
});
const isCurrentActorMember = computed((): boolean => {
if (!group.value || !memberships.value) return false;
return (memberships.value ?? [])
.map(({ parent: { id } }) => id)
.includes(group.value.id);
});
const isCurrentActorAGroupModerator = computed((): boolean => {
return hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
});
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>

View File

@@ -0,0 +1,523 @@
<template>
<div class="container mx-auto px-1 mb-6">
<h1>
{{ t("My events") }}
</h1>
<p>
{{
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="my-2" v-if="!hideCreateEventButton">
<o-button
tag="router-link"
variant="primary"
:to="{ name: RouteName.CREATE_EVENT }"
>{{ t("Create event") }}</o-button
>
</div>
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
<div class="flex flex-wrap gap-4 items-start">
<div
class="rounded p-3 flex-auto md:flex-none bg-zinc-300 dark:bg-zinc-700"
>
<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')
"
labelFor="events-start-datepicker"
>
<o-datepicker
v-model="dateFilter"
:first-day-of-week="firstDayOfWeek"
id="events-start-datepicker"
/>
<o-button
@click="dateFilter = new Date()"
class="reset-area !h-auto"
icon-left="close"
:title="t('Clear date filter field')"
/>
</o-field>
</div>
<div class="flex-1 min-w-[300px]">
<section
class="py-4 first:pt-0"
v-if="showUpcoming && showDrafts && drafts && drafts.total > 0"
>
<h2 class="text-2xl mb-2">{{ t("Drafts") }}</h2>
<multi-event-minimalist-card
:events="drafts.elements"
:showOrganizer="true"
/>
<o-pagination
class="mt-4"
v-show="drafts.total > LOGGED_USER_DRAFTS_LIMIT"
:total="drafts.total"
v-model:current="draftsPage"
:per-page="LOGGED_USER_DRAFTS_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</section>
<section
class="py-4 first:pt-0"
v-if="
showUpcoming && monthlyFutureEvents && monthlyFutureEvents.size > 0
"
>
<transition-group name="list" tag="p">
<div
class="mb-5"
v-for="month of monthlyFutureEvents"
:key="month[0]"
>
<h2 class="text-2xl">{{ month[0] }}</h2>
<div v-for="element in month[1]" :key="element.id">
<event-participation-card
v-if="'role' in element"
:participation="element"
@event-deleted="eventDeleted"
class="participation"
/>
<event-minimalist-card
v-else-if="
element.id &&
!monthParticipationsIds(month[1]).includes(element?.id)
"
:event="element"
class="participation"
/>
</div>
</div>
</transition-group>
<div class="columns is-centered">
<o-button
class="column is-narrow"
v-if="
hasMoreFutureParticipations &&
futureParticipations &&
futureParticipations.length === limit
"
@click="loadMoreFutureParticipations"
size="large"
variant="primary"
>{{ t("Load more") }}</o-button
>
</div>
</section>
<section
class="text-center not-found"
v-if="
showUpcoming &&
monthlyFutureEvents &&
monthlyFutureEvents.size === 0 &&
true // !$apollo.loading
"
>
<div class="img-container h-64 prose" />
<div class="text-center prose dark:prose-invert max-w-full">
<p>
{{
t(
"You don't have any upcoming events. Maybe try another filter?"
)
}}
</p>
<i18n-t
keypath="Do you wish to {create_event} or {explore_events}?"
tag="p"
>
<template #create_event>
<router-link :to="{ name: RouteName.CREATE_EVENT }">{{
t("create an event")
}}</router-link>
</template>
<template #explore_events>
<router-link
:to="{
name: RouteName.SEARCH,
query: { contentType: ContentType.EVENTS },
}"
>{{ t("explore the events") }}</router-link
>
</template>
</i18n-t>
</div>
</section>
<section v-if="!showUpcoming && pastParticipations.elements.length > 0">
<transition-group name="list" tag="p">
<div v-for="month in monthlyPastParticipations" :key="month[0]">
<h2 class="capitalize inline-block relative">{{ month[0] }}</h2>
<event-participation-card
v-for="participation in month[1]"
:key="participation.id"
:participation="participation as IParticipant"
:options="{ hideDate: false }"
@event-deleted="eventDeleted"
class="participation"
/>
</div>
</transition-group>
<div class="columns is-centered">
<o-button
class="column is-narrow"
v-if="
hasMorePastParticipations &&
pastParticipations.elements.length === limit
"
@click="loadMorePastParticipations"
size="large"
variant="primary"
>{{ t("Load more") }}</o-button
>
</div>
</section>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ParticipantRole, ContentType } from "@/types/enums";
import RouteName from "@/router/name";
import type { IParticipant } from "../../types/participant.model";
import { LOGGED_USER_DRAFTS } from "../../graphql/actor";
import type { IEvent } from "../../types/event.model";
import MultiEventMinimalistCard from "../../components/Event/MultiEventMinimalistCard.vue";
import EventMinimalistCard from "../../components/Event/EventMinimalistCard.vue";
import {
LOGGED_USER_PARTICIPATIONS,
LOGGED_USER_UPCOMING_EVENTS,
} from "@/graphql/participant";
import { useApolloClient, useQuery } from "@vue/apollo-composable";
import { computed, inject, ref, defineAsyncComponent } from "vue";
import { IUser } from "@/types/current-user.model";
import {
booleanTransformer,
integerTransformer,
useRouteQuery,
} from "vue-use-route-query";
import { Locale } from "date-fns";
import { useI18n } from "vue-i18n";
import { useRestrictions } from "@/composition/apollo/config";
import { useHead } from "@vueuse/head";
const EventParticipationCard = defineAsyncComponent(
() => import("@/components/Event/EventParticipationCard.vue")
);
type Eventable = IParticipant | IEvent;
const { t } = useI18n({ useScope: "global" });
const futurePage = ref(1);
const pastPage = ref(1);
const limit = ref(10);
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();
},
toQuery(value: Date) {
const pad = (number: number) => {
if (number < 10) {
return "0" + number;
}
return number;
};
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(
value.getDate()
)}`;
},
});
const hasMoreFutureParticipations = ref(true);
const hasMorePastParticipations = ref(true);
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 LOGGED_USER_DRAFTS_LIMIT = 10;
const draftsPage = useRouteQuery("draftsPage", 1, integerTransformer);
const { result: draftsResult } = useQuery<{
loggedUser: Pick<IUser, "drafts">;
}>(
LOGGED_USER_DRAFTS,
() => ({ page: draftsPage.value, limit: LOGGED_USER_DRAFTS_LIMIT }),
() => ({ fetchPolicy: "cache-and-network" })
);
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,
}
);
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();
});
} 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());
};
const monthlyFutureEvents = computed((): Map<string, Eventable[]> => {
let eventable = [] as Eventable[];
if (showAttending.value) {
eventable = [...eventable, ...futureParticipations.value];
}
if (showMyGroups.value) {
eventable = [...eventable, ...groupEvents.value.map(({ event }) => event)];
}
return monthlyEvents(eventable);
});
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 apollo = useApolloClient();
const eventDeleted = (eventid: string): void => {
/**
* Remove event from upcoming event participations
*/
const upcomingEventsData = apollo.client.cache.readQuery<{
loggedUser: IUser;
}>({
query: LOGGED_USER_UPCOMING_EVENTS,
variables: () => ({
page: 1,
limit: 10,
afterDateTime: dateFilter.value,
}),
});
if (!upcomingEventsData) return;
const loggedUser = upcomingEventsData?.loggedUser;
const participations = loggedUser?.participations;
apollo.client.cache.writeQuery<{ loggedUser: IUser }>({
query: LOGGED_USER_UPCOMING_EVENTS,
variables: () => ({
page: 1,
limit: 10,
afterDateTime: dateFilter.value,
}),
data: {
loggedUser: {
...loggedUser,
participations: {
total: participations.total - 1,
elements: participations.elements.filter(
(participation) => participation.event.id !== eventid
),
},
},
},
});
/**
* Remove event from past event participations
*/
const participationData = apollo.client.cache.readQuery<{
loggedUser: Pick<IUser, "participations">;
}>({
query: LOGGED_USER_PARTICIPATIONS,
variables: () => ({ page: 1, limit: 10 }),
});
if (!participationData) return;
const loggedUser2 = participationData?.loggedUser;
const participations2 = loggedUser?.participations;
apollo.client.cache.writeQuery<{
loggedUser: Pick<IUser, "participations">;
}>({
query: LOGGED_USER_PARTICIPATIONS,
variables: () => ({ page: 1, limit: 10 }),
data: {
loggedUser: {
...loggedUser2,
participations: {
total: participations2.total - 1,
elements: participations2.elements.filter(
(participation) => participation.event.id !== eventid
),
},
},
},
});
};
const { restrictions } = useRestrictions();
const hideCreateEventButton = computed((): boolean => {
return restrictions.value?.onlyGroupsCanCreateEvents === true;
});
const dateFnsLocale = inject<Locale>("dateFnsLocale");
const firstDayOfWeek = computed((): number => {
return dateFnsLocale?.options?.weekStartsOn ?? 0;
});
useHead({
title: computed(() => t("My events")),
});
</script>
<style lang="scss">
.not-found {
.img-container {
background-image: url("../../../img/pics/event_creation-480w.webp");
@media (min-resolution: 2dppx) {
& {
background-image: url("../../../img/pics/event_creation-1024w.webp");
}
}
max-width: 450px;
height: 300px;
box-shadow: 0 0 8px 8px white inset;
background-size: cover;
border-radius: 10px;
margin: auto auto 1rem;
}
}
</style>

View File

@@ -0,0 +1,503 @@
<template>
<section class="container mx-auto" v-if="event">
<breadcrumbs-nav
:links="[
{ name: RouteName.MY_EVENTS, text: t('My events') },
{
name: RouteName.EVENT,
params: { uuid: event.uuid },
text: event.title,
},
{
name: RouteName.PARTICIPATIONS,
params: { uuid: event.uuid },
text: t('Participants'),
},
]"
/>
<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="EVERYTHING">
{{ t("Everything") }}
</option>
<option :value="ParticipantRole.CREATOR">
{{ t("Organizer") }}
</option>
<option :value="ParticipantRole.PARTICIPANT">
{{ t("Participant") }}
</option>
<option :value="ParticipantRole.NOT_APPROVED">
{{ t("Not approved") }}
</option>
<option :value="ParticipantRole.REJECTED">
{{ t("Rejected") }}
</option>
</o-select>
</o-field>
</div>
<div class="" v-if="exportFormats.length > 0">
<o-dropdown aria-role="list">
<template #trigger="{ active }">
<o-button
:label="t('Export')"
variant="primary"
:icon-right="active ? 'menu-up' : 'menu-down'"
/>
</template>
<o-dropdown-item
has-link
v-for="format in exportFormats"
:key="format"
aria-role="listitem"
@click="
exportParticipants({
eventId: event.id ?? '',
format,
})
"
@keyup.enter="
exportParticipants({
eventId: event.id ?? '',
format,
})
"
>
<button class="dropdown-button">
<o-icon :icon="formatToIcon(format)"></o-icon>
{{ format }}
</button>
</o-dropdown-item>
</o-dropdown>
</div>
</div>
</div>
<o-table
:data="event.participants.elements"
ref="queueTable"
detailed
detail-key="id"
v-model:checked-rows="checkedRows"
checkable
:is-row-checkable="
(row: IParticipant) => row.role !== ParticipantRole.CREATOR
"
checkbox-position="left"
:show-detail-icon="false"
: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')"
:total="event.participants.total"
:per-page="PARTICIPANTS_PER_PAGE"
backend-sorting
:default-sort-direction="'desc'"
:default-sort="['insertedAt', 'desc']"
@page-change="(newPage: number) => (page = newPage)"
@sort="(field: string, order: string) => emit('sort', field, order)"
>
<o-table-column
field="actor.preferredUsername"
:label="t('Participant')"
v-slot="props"
>
<article class="flex gap-2">
<figure v-if="props.row.actor.avatar">
<img
class="rounded-full w-12 h-12 object-cover"
:src="props.row.actor.avatar.url"
alt=""
height="48"
width="48"
/>
</figure>
<Incognito
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
:size="48"
/>
<AccountCircle v-else :size="48" />
<div>
<div class="prose dark:prose-invert">
<p v-if="props.row.actor.preferredUsername !== 'anonymous'">
<span v-if="props.row.actor.name">{{
props.row.actor.name
}}</span
><br />
<span class="text-sm"
>@{{ usernameWithDomain(props.row.actor) }}</span
>
</p>
<span v-else>
{{ t("Anonymous participant") }}
</span>
</div>
</div>
</article>
</o-table-column>
<o-table-column field="role" :label="t('Role')" v-slot="props">
<tag
variant="primary"
v-if="props.row.role === ParticipantRole.CREATOR"
>
{{ t("Organizer") }}
</tag>
<tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
{{ t("Participant") }}
</tag>
<tag v-else-if="props.row.role === ParticipantRole.NOT_CONFIRMED">
{{ t("Not confirmed") }}
</tag>
<tag
variant="warning"
v-else-if="props.row.role === ParticipantRole.NOT_APPROVED"
>
{{ t("Not approved") }}
</tag>
<tag
variant="danger"
v-else-if="props.row.role === ParticipantRole.REJECTED"
>
{{ t("Rejected") }}
</tag>
</o-table-column>
<o-table-column
field="metadata.message"
class="column-message"
:label="t('Message')"
v-slot="props"
>
<div
@click="toggleQueueDetails(props.row)"
:class="{
'ellipsed-message':
props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH,
}"
v-if="props.row.metadata && props.row.metadata.message"
>
<p v-if="props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH">
{{ ellipsize(props.row.metadata.message) }}
</p>
<p v-else>
{{ props.row.metadata.message }}
</p>
<button
type="button"
class="button is-text"
v-if="props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH"
@click.stop="toggleQueueDetails(props.row)"
>
{{
openDetailedRows[props.row.id] ? t("View less") : t("View more")
}}
</button>
</div>
<p v-else class="has-text-grey-dark">
{{ t("No message") }}
</p>
</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>
</o-table-column>
<template #detail="props">
<article v-html="nl2br(props.row.metadata.message)" />
</template>
<template #empty>
<EmptyContent icon="account-circle" :inline="true">
{{ t("No participant matches the filters") }}
</EmptyContent>
</template>
<template #bottom-left>
<div class="flex gap-2">
<o-button
@click="acceptParticipants(checkedRows)"
variant="success"
:disabled="!canAcceptParticipants"
>
{{
t(
"No participant to approve|Approve participant|Approve {number} participants",
{ number: checkedRows.length },
checkedRows.length
)
}}
</o-button>
<o-button
@click="refuseParticipants(checkedRows)"
variant="danger"
:disabled="!canRefuseParticipants"
>
{{
t(
"No participant to reject|Reject participant|Reject {number} participants",
{ number: checkedRows.length },
checkedRows.length
)
}}
</o-button>
</div>
</template>
</o-table>
<EventConversations :event="event" class="my-6" />
<NewPrivateMessage :event="event" />
</section>
</template>
<script lang="ts" setup>
import { ParticipantRole } from "@/types/enums";
import { IParticipant } from "@/types/participant.model";
import { IEvent } from "@/types/event.model";
import {
EXPORT_EVENT_PARTICIPATIONS,
PARTICIPANTS,
UPDATE_PARTICIPANT,
} from "@/graphql/event";
import { usernameWithDomain } from "@/types/actor";
import { nl2br } from "@/utils/html";
import { asyncForEach } from "@/utils/asyncForEach";
import RouteName from "@/router/name";
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";
import Tag from "@/components/TagElement.vue";
import { useHead } from "@vueuse/head";
import EventConversations from "../../components/Conversations/EventConversations.vue";
import NewPrivateMessage from "../../components/Participation/NewPrivateMessage.vue";
const PARTICIPANTS_PER_PAGE = 10;
const MESSAGE_ELLIPSIS_LENGTH = 130;
type exportFormat = "CSV" | "PDF" | "ODS";
const props = defineProps<{
eventId: string;
}>();
const emit = defineEmits(["sort"]);
const { t } = useI18n({ useScope: "global" });
const { currentActor } = useCurrentActorClient();
const participantsExportFormats = useParticipantsExportFormats();
const ellipsize = (text?: string) =>
text && text.substring(0, MESSAGE_ELLIPSIS_LENGTH).concat("");
const eventId = computed(() => props.eventId);
const ParticipantAllRoles = { ...ParticipantRole, EVERYTHING: "EVERYTHING" };
const page = useRouteQuery("page", 1, integerTransformer);
const role = useRouteQuery(
"role",
"EVERYTHING",
enumTransformer(ParticipantAllRoles)
);
const checkedRows = ref<IParticipant[]>([]);
const queueTable = ref();
const { result: participantsResult, loading: participantsLoading } = useQuery<{
event: IEvent;
}>(
PARTICIPANTS,
() => ({
uuid: eventId.value,
page: page.value,
limit: PARTICIPANTS_PER_PAGE,
roles: role.value === "EVERYTHING" ? undefined : 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 = [];
};
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<
{ exportEventParticipants: { path: string; format: string } },
{ eventId: string; format?: exportFormat; roles?: string[] }
>(EXPORT_EVENT_PARTICIPATIONS);
onExportParticipantsMutationDone(({ data }) => {
const path = data?.exportEventParticipants?.path;
const format = data?.exportEventParticipants?.format;
const link = window.origin + "/exports/" + format?.toLowerCase() + "/" + path;
console.debug(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);
}
});
const exportFormats = computed((): exportFormat[] => {
return (participantsExportFormats ?? []).map(
(key) => key.toUpperCase() as exportFormat
);
});
const 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
*/
const canAcceptParticipants = (): boolean => {
return checkedRows.value.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
*/
const canRefuseParticipants = (): boolean => {
return checkedRows.value.some(
(participant: IParticipant) => participant.role !== ParticipantRole.REJECTED
);
};
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 = ref<Record<string, boolean>>({});
useHead({
title: computed(() =>
t("Participants to {eventTitle}", { eventTitle: event.value?.title })
),
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
section.container.container {
padding: 1rem;
}
.table {
.column-message {
vertical-align: middle;
}
.ellipsed-message {
cursor: pointer;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
p {
flex: 1;
min-width: 200px;
}
button {
display: inline;
}
}
}
nav.breadcrumb {
a {
text-decoration: none;
}
}
</style>