Participation panel revamp and fixes
Apollo is a pain in the ass with pagination & filters, so this removes the tabs system and uses a <select> to filter instead Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
8676582080
commit
b61d12b5fd
|
@ -6,8 +6,6 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
||||
<!-- <%= VUE_APP_INJECT_COMMENT %> -->
|
||||
<meta name="server-injected-data" />
|
||||
</head>
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ a {
|
|||
&[href="#comments"],
|
||||
&.router-link-active,
|
||||
&.comment-link,
|
||||
&.pagination-link {
|
||||
&.pagination-link,
|
||||
&.datepicker-cell {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
<b-button
|
||||
:disabled="newComment.text.trim().length === 0"
|
||||
native-type="submit"
|
||||
type="is-info"
|
||||
type="is-primary"
|
||||
>{{ $t("Post a reply") }}</b-button
|
||||
>
|
||||
</span>
|
||||
|
|
|
@ -60,10 +60,17 @@
|
|||
>
|
||||
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
|
||||
{{
|
||||
$t("{approved} / {total} seats", {
|
||||
approved: participation.event.participantStats.participant,
|
||||
total: participation.event.options.maximumAttendeeCapacity,
|
||||
})
|
||||
$tc(
|
||||
"{available}/{capacity} available places",
|
||||
participation.event.options.maximumAttendeeCapacity -
|
||||
(participation.event.participantStats.going - 1),
|
||||
{
|
||||
available:
|
||||
participation.event.options.maximumAttendeeCapacity -
|
||||
(participation.event.participantStats.going - 1),
|
||||
capacity: participation.event.options.maximumAttendeeCapacity,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
|
@ -79,6 +86,7 @@
|
|||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
query: { role: ParticipantRole.NOT_APPROVED },
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
|
|
|
@ -1,205 +0,0 @@
|
|||
<template>
|
||||
<b-table
|
||||
:data="data"
|
||||
ref="queueTable"
|
||||
detailed
|
||||
detail-key="id"
|
||||
:checked-rows.sync="checkedRows"
|
||||
checkable
|
||||
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
:show-detail-icon="false"
|
||||
:loading="this.$apollo.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="total"
|
||||
:per-page="perPage"
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="(page) => $emit('page-change', page)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" sortable>
|
||||
<b-tag type="is-success" class="has-text-centered"
|
||||
>{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
}}</b-tag
|
||||
>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" sortable v-if="showRole">
|
||||
<span v-if="props.row.role === ParticipantRole.CREATOR">
|
||||
{{ $t("Organizer") }}
|
||||
</span>
|
||||
<span v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ $t("Participant") }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<b-table-column field="actor.preferredUsername" :label="$t('Participant')" sortable>
|
||||
<article class="media">
|
||||
<figure class="media-left image is-48x48" v-if="props.row.actor.avatar">
|
||||
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
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" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>@{{ props.row.actor.preferredUsername }}</span
|
||||
>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("Anonymous participant") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="metadata.message" :label="$t('Message')">
|
||||
<span
|
||||
@click="toggleQueueDetails(props.row)"
|
||||
:class="{
|
||||
'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH,
|
||||
}"
|
||||
v-if="props.row.metadata && props.row.metadata.message"
|
||||
>
|
||||
{{ props.row.metadata.message | ellipsize }}
|
||||
</span>
|
||||
<span v-else class="has-text-grey">
|
||||
{{ $t("No message") }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article v-html="nl2br(props.row.metadata.message)" />
|
||||
</template>
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
@click="acceptParticipants(checkedRows)"
|
||||
type="is-success"
|
||||
v-if="canAcceptParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No participant to approve|Approve participant|Approve {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
<b-button
|
||||
@click="refuseParticipants(checkedRows)"
|
||||
type="is-danger"
|
||||
v-if="canRefuseParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No participant to reject|Reject participant|Reject {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
import { IParticipant, ParticipantRole } from "../../types/event.model";
|
||||
import { nl2br } from "../../utils/html";
|
||||
import { asyncForEach } from "../../utils/asyncForEach";
|
||||
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat("…"),
|
||||
},
|
||||
})
|
||||
export default class ParticipationTable extends Vue {
|
||||
@Prop({ required: true, type: Array }) data!: IParticipant[];
|
||||
|
||||
@Prop({ required: true, type: Number }) total!: number;
|
||||
|
||||
@Prop({ required: true, type: Function }) acceptParticipant!: Function;
|
||||
|
||||
@Prop({ required: true, type: Function }) refuseParticipant!: Function;
|
||||
|
||||
@Prop({ required: false, type: Boolean, default: false }) showRole!: boolean;
|
||||
|
||||
@Prop({ required: false, type: Number, default: 20 }) perPage!: number;
|
||||
|
||||
@Ref("queueTable") readonly queueTable!: any;
|
||||
|
||||
checkedRows: IParticipant[] = [];
|
||||
|
||||
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
|
||||
|
||||
nl2br = nl2br;
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
toggleQueueDetails(row: IParticipant) {
|
||||
if (row.metadata.message && row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH) return;
|
||||
this.queueTable.toggleDetails(row);
|
||||
}
|
||||
|
||||
async acceptParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.acceptParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async refuseParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.refuseParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ellipsed-message {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table {
|
||||
span.tag {
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -212,6 +212,7 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
|
|||
}
|
||||
}
|
||||
participantStats {
|
||||
going
|
||||
notApproved
|
||||
participant
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import gql from "graphql-tag";
|
||||
import { COMMENT_FIELDS_FRAGMENT } from "@/graphql/comment";
|
||||
|
||||
const participantQuery = `
|
||||
role,
|
||||
|
@ -466,6 +465,8 @@ export const PARTICIPANTS = gql`
|
|||
query($uuid: UUID!, $page: Int, $limit: Int, $roles: String, $actorId: ID!) {
|
||||
event(uuid: $uuid) {
|
||||
id,
|
||||
uuid,
|
||||
title,
|
||||
participants(page: $page, limit: $limit, roles: $roles, actorId: $actorId) {
|
||||
${participantsQuery}
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ export enum UserRouteName {
|
|||
RESEND_CONFIRMATION = "ResendConfirmation",
|
||||
SEND_PASSWORD_RESET = "SendPasswordReset",
|
||||
PASSWORD_RESET = "PasswordReset",
|
||||
EMAIL_VALIDATE = "EMAIL_VALIDATE",
|
||||
VALIDATE = "Validate",
|
||||
LOGIN = "Login",
|
||||
}
|
||||
|
@ -54,7 +55,7 @@ export const userRoutes: RouteConfig[] = [
|
|||
},
|
||||
{
|
||||
path: "/validate/email/:token",
|
||||
name: UserRouteName.VALIDATE,
|
||||
name: UserRouteName.EMAIL_VALIDATE,
|
||||
component: () => import("@/views/User/EmailValidate.vue"),
|
||||
props: true,
|
||||
meta: { requiresAuth: false },
|
||||
|
|
|
@ -168,14 +168,17 @@
|
|||
v-if="actorIsOrganizer && event.draft === false"
|
||||
:to="{ name: RouteName.PARTICIPATIONS, params: { eventId: event.uuid } }"
|
||||
>
|
||||
<!-- We retire one because of the event creator who is a participant -->
|
||||
<span v-if="event.options.maximumAttendeeCapacity">
|
||||
{{
|
||||
$tc(
|
||||
"{available}/{capacity} available places",
|
||||
event.options.maximumAttendeeCapacity - event.participantStats.going,
|
||||
event.options.maximumAttendeeCapacity -
|
||||
(event.participantStats.going - 1),
|
||||
{
|
||||
available:
|
||||
event.options.maximumAttendeeCapacity - event.participantStats.going,
|
||||
event.options.maximumAttendeeCapacity -
|
||||
(event.participantStats.going - 1),
|
||||
capacity: event.options.maximumAttendeeCapacity,
|
||||
}
|
||||
)
|
||||
|
@ -183,8 +186,8 @@
|
|||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
$tc("No one is going to this event", event.participantStats.going, {
|
||||
going: event.participantStats.going,
|
||||
$tc("No one is going to this event", event.participantStats.going - 1, {
|
||||
going: event.participantStats.going - 1,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
|
@ -226,7 +229,7 @@
|
|||
</p>
|
||||
<b-dropdown position="is-bottom-left" aria-role="list">
|
||||
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
|
||||
Actions
|
||||
{{ $t("Actions") }}
|
||||
<!-- <b-icon icon="dots-horizontal" /> -->
|
||||
</b-button>
|
||||
<b-dropdown-item
|
||||
|
|
|
@ -1,77 +1,189 @@
|
|||
import {ParticipantRole} from "@/types/event.model";
|
||||
<template>
|
||||
<main class="container">
|
||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple" />
|
||||
<span
|
||||
>{{ $t("Participants") }} <b-tag rounded> {{ participantStats.going }} </b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="participants && participants.total > 0">
|
||||
<section v-if="event">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_EVENTS }">{{ $t("My events") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: event.uuid },
|
||||
}"
|
||||
>{{ event.title }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PARTICIPANTS,
|
||||
params: { uuid: event.uuid },
|
||||
}"
|
||||
>{{ $t("Participants") }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2 class="title">{{ $t("Participants") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="participants.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:showRole="true"
|
||||
:total="participants.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (participantPage = page)"
|
||||
<b-field :label="$t('Status')" horizontal>
|
||||
<b-select v-model="roles">
|
||||
<option value="">
|
||||
{{ $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>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-table
|
||||
:data="event.participants.elements"
|
||||
ref="queueTable"
|
||||
detailed
|
||||
detail-key="id"
|
||||
:checked-rows.sync="checkedRows"
|
||||
checkable
|
||||
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
:show-detail-icon="false"
|
||||
:loading="this.$apollo.loading"
|
||||
paginated
|
||||
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) => (page = newPage)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="actor.preferredUsername" :label="$t('Participant')">
|
||||
<article class="media">
|
||||
<figure class="media-left image is-48x48" v-if="props.row.actor.avatar">
|
||||
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
|
||||
size="is-large"
|
||||
icon="incognito"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.notApproved === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-plus" />
|
||||
<span
|
||||
>{{ $t("Requests") }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>@{{ props.row.actor.preferredUsername }}</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="queue && queue.total > 0">
|
||||
<h2 class="title">{{ $t("Waiting list") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="queue.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="queue.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (queuePage = page)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.rejected === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-minus"></b-icon>
|
||||
<span
|
||||
>{{ $t("Rejected") }} <b-tag rounded> {{ participantStats.rejected }} </b-tag>
|
||||
<span v-else>
|
||||
{{ $t("Anonymous participant") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')">
|
||||
<b-tag type="is-primary" v-if="props.row.role === ParticipantRole.CREATOR">
|
||||
{{ $t("Organizer") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ $t("Participant") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-warning" v-else-if="props.row.role === ParticipantRole.NOT_APPROVED">
|
||||
{{ $t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-danger" v-else-if="props.row.role === ParticipantRole.REJECTED">
|
||||
{{ $t("Rejected") }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="metadata.message" :label="$t('Message')">
|
||||
<span
|
||||
@click="toggleQueueDetails(props.row)"
|
||||
:class="{
|
||||
'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH,
|
||||
}"
|
||||
v-if="props.row.metadata && props.row.metadata.message"
|
||||
>
|
||||
{{ props.row.metadata.message | ellipsize }}
|
||||
</span>
|
||||
<span v-else class="has-text-grey">
|
||||
{{ $t("No message") }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<b-table-column field="insertedAt" :label="$t('Date')">
|
||||
<span class="has-text-centered">
|
||||
{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
}}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="rejected && rejected.total > 0">
|
||||
<h2 class="title">{{ $t("Rejected participations") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="rejected.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="rejected.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (rejectedPage = page)"
|
||||
/>
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article v-html="nl2br(props.row.metadata.message)" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No participant matches the filters") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
</b-tabs>
|
||||
<template slot="bottom-left">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
@click="acceptParticipants(checkedRows)"
|
||||
type="is-success"
|
||||
:disabled="!canAcceptParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No participant to approve|Approve participant|Approve {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
<b-button
|
||||
@click="refuseParticipants(checkedRows)"
|
||||
type="is-danger"
|
||||
:disabled="!canRefuseParticipants"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No participant to reject|Reject participant|Reject {number} participants",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { Component, Prop, Vue, Watch, Ref } from "vue-property-decorator";
|
||||
import {
|
||||
IEvent,
|
||||
IEventParticipantStats,
|
||||
|
@ -85,15 +197,17 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
|||
import { IPerson } from "../../types/actor";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import ParticipationTable from "../../components/Event/ParticipationTable.vue";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { DataProxy } from "apollo-cache";
|
||||
import { nl2br } from "../../utils/html";
|
||||
import { asyncForEach } from "../../utils/asyncForEach";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 20;
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ParticipationTable,
|
||||
ParticipantCard,
|
||||
},
|
||||
apollo: {
|
||||
|
@ -103,12 +217,13 @@ const MESSAGE_ELLIPSIS_LENGTH = 130;
|
|||
config: CONFIG,
|
||||
event: {
|
||||
query: PARTICIPANTS,
|
||||
fetchPolicy: "network-only",
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.PARTICIPANT].join(),
|
||||
roles: this.roles,
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
|
@ -116,60 +231,6 @@ const MESSAGE_ELLIPSIS_LENGTH = 130;
|
|||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
participants: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: this.participantPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.CREATOR, ParticipantRole.PARTICIPANT].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
queue: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: this.queuePage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.NOT_APPROVED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
rejected: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: this.rejectedPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.REJECTED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat("…"),
|
||||
|
@ -180,19 +241,7 @@ export default class Participants extends Vue {
|
|||
|
||||
page = 1;
|
||||
|
||||
limit = 10;
|
||||
|
||||
participants!: Paginate<IParticipant>;
|
||||
|
||||
participantPage = 1;
|
||||
|
||||
queue!: Paginate<IParticipant>;
|
||||
|
||||
queuePage = 1;
|
||||
|
||||
rejected!: Paginate<IParticipant>;
|
||||
|
||||
rejectedPage = 1;
|
||||
limit = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
event!: IEvent;
|
||||
|
||||
|
@ -202,19 +251,21 @@ export default class Participants extends Vue {
|
|||
|
||||
currentActor!: IPerson;
|
||||
|
||||
hasMoreParticipants = false;
|
||||
|
||||
activeTab = 0;
|
||||
|
||||
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
dataTransform(data: { event: IEvent }): Paginate<Participant> {
|
||||
return {
|
||||
total: data.event.participants.total,
|
||||
elements: data.event.participants.elements.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
),
|
||||
};
|
||||
checkedRows: IParticipant[] = [];
|
||||
|
||||
roles: ParticipantRole = ParticipantRole.PARTICIPANT;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
@Ref("queueTable") readonly queueTable!: any;
|
||||
|
||||
mounted() {
|
||||
const roleQuery = this.$route.query.role as string;
|
||||
if (Object.values(ParticipantRole).includes(roleQuery as ParticipantRole)) {
|
||||
this.roles = roleQuery as ParticipantRole;
|
||||
}
|
||||
}
|
||||
|
||||
get participantStats(): IEventParticipantStats | null {
|
||||
|
@ -222,20 +273,9 @@ export default class Participants extends Vue {
|
|||
return this.event.participantStats;
|
||||
}
|
||||
|
||||
@Watch("participantStats", { deep: true })
|
||||
watchParticipantStats(stats: IEventParticipantStats) {
|
||||
if (!stats) return;
|
||||
if (
|
||||
(stats.notApproved === 0 && this.activeTab === 1) ||
|
||||
(stats.rejected === 0 && this.activeTab === 2)
|
||||
) {
|
||||
this.activeTab = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("page")
|
||||
loadMoreParticipants() {
|
||||
this.page += 1;
|
||||
this.$apollo.queries.participants.fetchMore({
|
||||
this.$apollo.queries.event.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
page: this.page,
|
||||
|
@ -243,13 +283,17 @@ export default class Participants extends Vue {
|
|||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newParticipations = fetchMoreResult.event.participants;
|
||||
this.hasMoreParticipants = newParticipations.length === this.limit;
|
||||
const oldParticipants = previousResult.event.participants;
|
||||
const newParticipants = fetchMoreResult.event.participants;
|
||||
|
||||
return {
|
||||
loggedUser: {
|
||||
__typename: previousResult.event.__typename,
|
||||
participations: [...previousResult.event.participants, ...newParticipations],
|
||||
event: {
|
||||
...previousResult.event,
|
||||
participants: {
|
||||
elements: [...oldParticipants.elements, ...newParticipants.elements],
|
||||
total: newParticipants.total,
|
||||
__typename: oldParticipants.__typename,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -266,23 +310,6 @@ export default class Participants extends Vue {
|
|||
role: ParticipantRole.PARTICIPANT,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.queue.elements = this.queue.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.rejected.elements = this.rejected.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.event.participantStats.going += 1;
|
||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
}
|
||||
if (participant.role === ParticipantRole.REJECTED) {
|
||||
this.event.participantStats.rejected -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.PARTICIPANT;
|
||||
this.event.participants.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -298,52 +325,77 @@ export default class Participants extends Vue {
|
|||
role: ParticipantRole.REJECTED,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.event.participants.elements = this.event.participants.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.queue.elements = this.queue.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.event.participantStats.rejected += 1;
|
||||
if (participant.role === ParticipantRole.PARTICIPANT) {
|
||||
this.event.participantStats.participant -= 1;
|
||||
this.event.participantStats.going -= 1;
|
||||
}
|
||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.REJECTED;
|
||||
this.rejected.elements = this.rejected.elements.filter(
|
||||
(participantIn) => participantIn.id !== participant.id
|
||||
);
|
||||
this.rejected.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.acceptParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async refuseParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.refuseParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (row.metadata.message && row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH) return;
|
||||
this.queueTable.toggleDetails(row);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables.scss";
|
||||
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/deep/ .tabs.is-boxed {
|
||||
.table {
|
||||
.ellipsed-message {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.tag {
|
||||
&.is-primary {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav.breadcrumb {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
nav.tabs li {
|
||||
margin: 3rem 0 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -92,7 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
|> Enum.map(&String.to_existing_atom/1)
|
||||
end
|
||||
|
||||
{:ok, Events.list_participants_for_event(event_id, roles, page, limit)}
|
||||
participants = Events.list_participants_for_event(event_id, roles, page, limit)
|
||||
{:ok, participants}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Moderator Actor ID is not owned by authenticated user"}
|
||||
|
@ -115,17 +116,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
_args,
|
||||
%{context: %{current_user: %User{id: user_id} = _user}} = _resolution
|
||||
) do
|
||||
if Events.is_user_moderator_for_event?(user_id, event_id) do
|
||||
stats =
|
||||
Map.put(
|
||||
stats,
|
||||
:going,
|
||||
stats.participant + stats.moderator + stats.administrator + stats.creator
|
||||
)
|
||||
going = stats.participant + stats.moderator + stats.administrator + stats.creator
|
||||
|
||||
{:ok, stats}
|
||||
if Events.is_user_moderator_for_event?(user_id, event_id) do
|
||||
{:ok, Map.put(stats, :going, going)}
|
||||
else
|
||||
{:ok, %EventParticipantStats{}}
|
||||
{:ok, %{going: going}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -752,7 +752,6 @@ defmodule Mobilizon.Events do
|
|||
end
|
||||
|
||||
@moderator_roles [:moderator, :administrator, :creator]
|
||||
@default_participant_roles [:participant] ++ @moderator_roles
|
||||
|
||||
@doc """
|
||||
Returns the list of participants for an event.
|
||||
|
@ -762,13 +761,14 @@ defmodule Mobilizon.Events do
|
|||
Page.t()
|
||||
def list_participants_for_event(
|
||||
id,
|
||||
roles \\ @default_participant_roles,
|
||||
roles \\ [],
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
) do
|
||||
id
|
||||
|> list_participants_for_event_query()
|
||||
|> filter_role(roles)
|
||||
|> order_by(asc: :role)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ defmodule Mobilizon.Web.ErrorView do
|
|||
"""
|
||||
use Mobilizon.Web, :view
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
alias Mobilizon.Web.PageView
|
||||
import Mobilizon.Web.Views.Utils
|
||||
|
||||
def render("404.html", _assigns) do
|
||||
def render("404.html", %{conn: conn}) do
|
||||
tags = Instance.build_tags()
|
||||
PageView.inject_tags(tags)
|
||||
inject_tags(tags, get_locale(conn))
|
||||
end
|
||||
|
||||
def render("404.json", _assigns) do
|
||||
|
|
|
@ -13,10 +13,10 @@ defmodule Mobilizon.Web.PageView do
|
|||
|
||||
alias Mobilizon.Service.Metadata
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
alias Mobilizon.Service.Metadata.Utils, as: MetadataUtils
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Utils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
import Mobilizon.Web.Views.Utils
|
||||
|
||||
def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do
|
||||
actor
|
||||
|
@ -59,38 +59,4 @@ defmodule Mobilizon.Web.PageView do
|
|||
tags = Instance.build_tags()
|
||||
inject_tags(tags, get_locale(conn))
|
||||
end
|
||||
|
||||
@spec inject_tags(List.t(), String.t()) :: {:safe, String.t()}
|
||||
def inject_tags(tags, locale \\ "en") do
|
||||
with {:ok, index_content} <- File.read(index_file_path()) do
|
||||
do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale)
|
||||
end
|
||||
end
|
||||
|
||||
@spec index_file_path :: String.t()
|
||||
defp index_file_path do
|
||||
Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html")
|
||||
end
|
||||
|
||||
@spec replace_meta(String.t(), String.t()) :: String.t()
|
||||
# TODO: Find why it's different in dev/prod and during tests
|
||||
defp replace_meta(index_content, tags) do
|
||||
index_content
|
||||
|> String.replace("<meta name=\"server-injected-data\" />", tags)
|
||||
|> String.replace("<meta name=server-injected-data>", tags)
|
||||
end
|
||||
|
||||
@spec do_replacements(String.t(), String.t(), String.t()) :: {:safe, String.t()}
|
||||
defp do_replacements(index_content, tags, locale) do
|
||||
index_content
|
||||
|> replace_meta(tags)
|
||||
|> String.replace("<html lang=\"en\">", "<html lang=\"#{locale}\">")
|
||||
|> String.replace("<html lang=en>", "<html lang=\"#{locale}\">")
|
||||
|> (&{:safe, &1}).()
|
||||
end
|
||||
|
||||
@spec get_locale(Conn.t()) :: String.t()
|
||||
defp get_locale(%{private: %{cldr_locale: nil}}), do: "en"
|
||||
defp get_locale(%{private: %{cldr_locale: %{requested_locale_name: locale}}}), do: locale
|
||||
defp get_locale(_), do: "en"
|
||||
end
|
||||
|
|
41
lib/web/views/utils.ex
Normal file
41
lib/web/views/utils.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule Mobilizon.Web.Views.Utils do
|
||||
@moduledoc """
|
||||
Utils for views
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Metadata.Utils, as: MetadataUtils
|
||||
|
||||
@spec inject_tags(List.t(), String.t()) :: {:safe, String.t()}
|
||||
def inject_tags(tags, locale \\ "en") do
|
||||
with {:ok, index_content} <- File.read(index_file_path()) do
|
||||
do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale)
|
||||
end
|
||||
end
|
||||
|
||||
@spec index_file_path :: String.t()
|
||||
defp index_file_path do
|
||||
Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html")
|
||||
end
|
||||
|
||||
@spec replace_meta(String.t(), String.t()) :: String.t()
|
||||
# TODO: Find why it's different in dev/prod and during tests
|
||||
defp replace_meta(index_content, tags) do
|
||||
index_content
|
||||
|> String.replace("<meta name=\"server-injected-data\" />", tags)
|
||||
|> String.replace("<meta name=server-injected-data>", tags)
|
||||
end
|
||||
|
||||
@spec do_replacements(String.t(), String.t(), String.t()) :: {:safe, String.t()}
|
||||
defp do_replacements(index_content, tags, locale) do
|
||||
index_content
|
||||
|> replace_meta(tags)
|
||||
|> String.replace("<html lang=\"en\">", "<html lang=\"#{locale}\">")
|
||||
|> String.replace("<html lang=en>", "<html lang=\"#{locale}\">")
|
||||
|> (&{:safe, &1}).()
|
||||
end
|
||||
|
||||
@spec get_locale(Conn.t()) :: String.t()
|
||||
def get_locale(%{private: %{cldr_locale: nil}}), do: "en"
|
||||
def get_locale(%{private: %{cldr_locale: %{requested_locale_name: locale}}}), do: locale
|
||||
def get_locale(_), do: "en"
|
||||
end
|
|
@ -2,7 +2,7 @@ defmodule Mobilizon.Storage.Repo.Migrations.RenamePostgresTypes do
|
|||
use Ecto.Migration
|
||||
alias Mobilizon.Actors.{ActorVisibility, MemberRole}
|
||||
|
||||
alias alias Mobilizon.Conversations.CommentVisibility
|
||||
alias Mobilizon.Conversations.CommentVisibility
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
JoinOptions,
|
||||
|
|
|
@ -1365,8 +1365,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
|||
assert event.id
|
||||
|> Events.list_participants_for_event()
|
||||
|> Map.get(:elements)
|
||||
|> Enum.map(& &1.id) ==
|
||||
[]
|
||||
|> Enum.map(& &1.role) == [:rejected]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue