modify event view & event participation with new permission - #687

This commit is contained in:
Laurent GAY
2025-11-12 21:18:09 +01:00
committed by setop
parent 7b6c06d233
commit 5a020ae216
7 changed files with 246 additions and 48 deletions

View File

@@ -21,17 +21,14 @@
@cancel-anonymous-participation="cancelAnonymousParticipation" @cancel-anonymous-participation="cancelAnonymousParticipation"
/> />
<div class="flex flex-col gap-1 mt-1"> <div class="flex flex-col gap-1 mt-1">
<p <p class="inline-flex gap-2 ml-auto" v-if="showParticipant">
class="inline-flex gap-2 ml-auto"
v-if="
event.joinOptions !== EventJoinOptions.EXTERNAL &&
!event.options.hideNumberOfParticipants
"
>
<TicketConfirmationOutline /> <TicketConfirmationOutline />
<router-link <router-link
class="participations-link" class="participations-link underline"
v-if="canManageEvent && event?.draft === false" v-if="
(insideGroupWithAllowSee || canManageEvent) &&
event?.draft === false
"
:to="{ :to="{
name: RouteName.PARTICIPATIONS, name: RouteName.PARTICIPATIONS,
params: { eventId: event.uuid }, params: { eventId: event.uuid },
@@ -110,20 +107,6 @@
{{ t("Actions") }} {{ t("Actions") }}
</o-button> </o-button>
</template> </template>
<o-dropdown-item
aria-role="listitem"
has-link
v-if="canManageEvent"
@click="
router.push({
name: RouteName.PARTICIPATIONS,
params: { eventId: event?.uuid },
})
"
>
<AccountMultiple />
{{ t("Participations") }}
</o-dropdown-item>
<o-dropdown-item <o-dropdown-item
aria-role="listitem" aria-role="listitem"
has-link has-link
@@ -481,6 +464,15 @@ const triggerShare = (): void => {
// @ts-ignore-end // @ts-ignore-end
}; };
const showParticipant = computed((): boolean => {
return (
insideGroupWithAllowSee.value ||
canManageEvent.value ||
(event.value?.joinOptions !== EventJoinOptions.EXTERNAL &&
!event.value?.options.hideNumberOfParticipants)
);
});
const canManageEvent = computed((): boolean => { const canManageEvent = computed((): boolean => {
return actorIsOrganizer.value || hasGroupPrivileges.value; return actorIsOrganizer.value || hasGroupPrivileges.value;
}); });
@@ -494,6 +486,14 @@ const canManageEvent = computed((): boolean => {
// ); // );
// }); // });
const insideGroupWithAllowSee = computed((): boolean => {
return (
event.value?.attributedTo?.allowSeeParticipants &&
props.person?.memberships !== undefined &&
props.person?.memberships?.total > 0
);
});
const actorIsOrganizer = computed((): boolean => { const actorIsOrganizer = computed((): boolean => {
return ( return (
props.participations.length > 0 && props.participations.length > 0 &&

View File

@@ -8,6 +8,7 @@ import {
} from "./participant"; } from "./participant";
import { TAG_FRAGMENT } from "./tags"; import { TAG_FRAGMENT } from "./tags";
import { CONVERSATIONS_QUERY_FRAGMENT } from "./conversations"; import { CONVERSATIONS_QUERY_FRAGMENT } from "./conversations";
import { GROUP_MINIMAL_FIELDS_FRAGMENTS } from "./group";
const FULL_EVENT_FRAGMENT = gql` const FULL_EVENT_FRAGMENT = gql`
fragment FullEvent on Event { fragment FullEvent on Event {
@@ -49,7 +50,7 @@ const FULL_EVENT_FRAGMENT = gql`
...ActorFragment ...ActorFragment
} }
attributedTo { attributedTo {
...ActorFragment ...GroupMinimalFields
} }
participantStats { participantStats {
going going
@@ -73,6 +74,7 @@ const FULL_EVENT_FRAGMENT = gql`
${TAG_FRAGMENT} ${TAG_FRAGMENT}
${EVENT_OPTIONS_FRAGMENT} ${EVENT_OPTIONS_FRAGMENT}
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
${GROUP_MINIMAL_FIELDS_FRAGMENTS}
`; `;
export const FETCH_EVENT = gql` export const FETCH_EVENT = gql`
@@ -146,7 +148,7 @@ export const FETCH_EVENTS = gql`
...ActorFragment ...ActorFragment
} }
attributedTo { attributedTo {
...ActorFragment ...GroupMinimalFields
} }
category category
tags { tags {
@@ -162,6 +164,7 @@ export const FETCH_EVENTS = gql`
${TAG_FRAGMENT} ${TAG_FRAGMENT}
${EVENT_OPTIONS_FRAGMENT} ${EVENT_OPTIONS_FRAGMENT}
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
${GROUP_MINIMAL_FIELDS_FRAGMENTS}
`; `;
export const CREATE_EVENT = gql` export const CREATE_EVENT = gql`
@@ -358,12 +361,13 @@ export const PARTICIPANTS = gql`
...ActorFragment ...ActorFragment
} }
attributedTo { attributedTo {
...ActorFragment ...GroupMinimalFields
} }
} }
} }
${PARTICIPANTS_QUERY_FRAGMENT} ${PARTICIPANTS_QUERY_FRAGMENT}
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
${GROUP_MINIMAL_FIELDS_FRAGMENTS}
`; `;
export const EVENT_PERSON_PARTICIPATION = gql` export const EVENT_PERSON_PARTICIPATION = gql`
@@ -445,7 +449,7 @@ export const FETCH_GROUP_EVENTS = gql`
notApproved notApproved
} }
attributedTo { attributedTo {
...ActorFragment ...GroupMinimalFields
} }
organizerActor { organizerActor {
...ActorFragment ...ActorFragment
@@ -466,6 +470,7 @@ export const FETCH_GROUP_EVENTS = gql`
${EVENT_OPTIONS_FRAGMENT} ${EVENT_OPTIONS_FRAGMENT}
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
${ADDRESS_FRAGMENT} ${ADDRESS_FRAGMENT}
${GROUP_MINIMAL_FIELDS_FRAGMENTS}
`; `;
export const EXPORT_EVENT_PARTICIPATIONS = gql` export const EXPORT_EVENT_PARTICIPATIONS = gql`

View File

@@ -53,6 +53,27 @@ export const LIST_GROUPS = gql`
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
`; `;
export const GROUP_MINIMAL_FIELDS_FRAGMENTS = gql`
fragment GroupMinimalFields on Group {
...ActorFragment
suspended
visibility
openness
manuallyApprovesFollowers
allowSeeParticipants
members {
elements {
id
role
actor {
id
}
}
}
}
${ACTOR_FRAGMENT}
`;
export const GROUP_VERY_BASIC_FIELDS_FRAGMENTS = gql` export const GROUP_VERY_BASIC_FIELDS_FRAGMENTS = gql`
fragment GroupVeryBasicFields on Group { fragment GroupVeryBasicFields on Group {
...ActorFragment ...ActorFragment

View File

@@ -82,7 +82,7 @@
detailed detailed
detail-key="id" detail-key="id"
v-model:checked-rows="checkedRows" v-model:checked-rows="checkedRows"
checkable :checkable="canChange"
:is-row-checkable=" :is-row-checkable="
(row: IParticipant) => row.role !== ParticipantRole.CREATOR (row: IParticipant) => row.role !== ParticipantRole.CREATOR
" "
@@ -221,7 +221,7 @@
</EmptyContent> </EmptyContent>
</template> </template>
</o-table> </o-table>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2" v-if="canChange">
<o-button <o-button
@click="acceptParticipants(checkedRows)" @click="acceptParticipants(checkedRows)"
variant="success" variant="success"
@@ -283,6 +283,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { Notifier } from "@/plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import Tag from "@/components/TagElement.vue"; import Tag from "@/components/TagElement.vue";
import { useHead } from "@/utils/head"; import { useHead } from "@/utils/head";
import { IMember } from "@/types/actor/member.model";
const PARTICIPANTS_PER_PAGE = 10; const PARTICIPANTS_PER_PAGE = 10;
const MESSAGE_ELLIPSIS_LENGTH = 130; const MESSAGE_ELLIPSIS_LENGTH = 130;
@@ -318,6 +319,14 @@ const checkedRows = ref<IParticipant[]>([]);
const queueTable = ref(); const queueTable = ref();
const is_enabled = computed((): boolean => {
return (
currentActor.value?.id !== undefined &&
page.value !== undefined &&
role.value !== undefined
);
});
const { const {
result: participantsResult, result: participantsResult,
loading: participantsLoading, loading: participantsLoading,
@@ -333,10 +342,7 @@ const {
roles: role.value === "EVERYTHING" ? undefined : role.value, roles: role.value === "EVERYTHING" ? undefined : role.value,
}), }),
() => ({ () => ({
enabled: enabled: is_enabled.value,
currentActor.value?.id !== undefined &&
page.value !== undefined &&
role.value !== undefined,
}) })
); );
@@ -354,6 +360,29 @@ const onPageChange = (p: number): void => {
const event = computed(() => participantsResult.value?.event); const event = computed(() => participantsResult.value?.event);
const getGroupMember = computed<IMember | undefined>(
(): IMember | undefined => {
const group_members: IMember[] | undefined =
event.value?.attributedTo?.members.elements.filter(
(member: IMember) =>
member.actor.id?.toString() === currentActor.value?.id?.toString()
);
if (group_members?.length > 0) {
return group_members[0];
} else {
return undefined;
}
}
);
const canChange = computed(() => {
if (event.value?.attributedTo?.allowSeeParticipants) {
return getGroupMember.value?.role !== "MEMBER";
} else {
return true;
}
});
// const participantStats = computed((): IEventParticipantStats | null => { // const participantStats = computed((): IEventParticipantStats | null => {
// if (!event.value) return null; // if (!event.value) return null;
// return event.value.participantStats; // return event.value.participantStats;

View File

@@ -175,27 +175,24 @@ describe("EventActionSection", () => {
expect(wrapper.find(".participations-link").text()).toBe( expect(wrapper.find(".participations-link").text()).toBe(
"No one is participating" "No one is participating"
); );
expect(wrapper.findAll("o-dropdown > o-dropdown-item").length).toBe(7); expect(wrapper.findAll("o-dropdown > o-dropdown-item").length).toBe(6);
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(1)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(1)").text()
).toBe("Participations");
expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(2)").text()
).toBe("Announcements"); ).toBe("Announcements");
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(3)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(2)").text()
).toBe("Edit"); ).toBe("Edit");
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(4)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(3)").text()
).toBe("Duplicate"); ).toBe("Duplicate");
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(5)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(4)").text()
).toBe("Delete"); ).toBe("Delete");
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(6)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(5)").text()
).toBe("Share this event"); ).toBe("Share this event");
expect( expect(
wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(7)").text() wrapper.find("o-dropdown > o-dropdown-item:nth-of-type(6)").text()
).toBe("Add to my calendar"); ).toBe("Add to my calendar");
expect( expect(
wrapper wrapper
@@ -209,4 +206,54 @@ describe("EventActionSection", () => {
); );
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
it("event action section with event's group without permission for user/member", async () => {
const wrapper = generateWrapper(
{
attributedTo: {
id: 123,
uuid: "987654314231132",
allowSeeParticipants: false,
},
options: {
hideNumberOfParticipants: true,
},
},
undefined,
[],
{
memberships: {
total: 1,
elements: [{ role: "MEMBER" }],
},
}
);
await wrapper.vm.$nextTick();
expect(wrapper.html()).toMatchSnapshot();
});
it("event action section with event's group with permission for user/member", async () => {
const wrapper = generateWrapper(
{
attributedTo: {
id: 123,
uuid: "987654314231132",
allowSeeParticipants: true,
},
options: {
hideNumberOfParticipants: true,
},
},
undefined,
[],
{
memberships: {
total: 1,
elements: [{ role: "MEMBER" }],
},
}
);
await wrapper.vm.$nextTick();
expect(wrapper.html()).toMatchSnapshot();
});
}); });

View File

@@ -14,7 +14,6 @@ exports[`EventActionSection > event action section with basic informations 1`] =
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if-->
<o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item> <o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item>
<o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item> <o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item>
<!--v-if--> <!--v-if-->
@@ -56,16 +55,13 @@ exports[`EventActionSection > event action section with creator as participant 1
<!--v-if--> <!--v-if-->
<div class="flex flex-col gap-1 mt-1"> <div class="flex flex-col gap-1 mt-1">
<p class="inline-flex gap-2 ml-auto"> <p class="inline-flex gap-2 ml-auto">
<ticket-confirmation-outline-stub fillcolor="currentColor" size="24"></ticket-confirmation-outline-stub><a href="/events/f37910ea-fd5a-4756-9679-00971f3f4106/participations" class="participations-link"> <ticket-confirmation-outline-stub fillcolor="currentColor" size="24"></ticket-confirmation-outline-stub><a href="/events/f37910ea-fd5a-4756-9679-00971f3f4106/participations" class="participations-link underline">
<!-- We retire one because of the event creator who is a <!-- We retire one because of the event creator who is a
participant --><span>No one is participating</span> participant --><span>No one is participating</span>
</a> </a>
<!--v-if--> <!--v-if-->
</p> </p>
<o-dropdown class="ml-auto"> <o-dropdown class="ml-auto">
<o-dropdown-item aria-role="listitem" has-link="">
<account-multiple-stub fillcolor="currentColor" size="24"></account-multiple-stub> Participations
</o-dropdown-item>
<o-dropdown-item aria-role="listitem" has-link=""> <o-dropdown-item aria-role="listitem" has-link="">
<bullhorn-stub fillcolor="currentColor" size="24"></bullhorn-stub> Announcements <bullhorn-stub fillcolor="currentColor" size="24"></bullhorn-stub> Announcements
</o-dropdown-item> </o-dropdown-item>
@@ -113,6 +109,106 @@ exports[`EventActionSection > event action section with creator as participant 1
</o-modal>" </o-modal>"
`; `;
exports[`EventActionSection > event action section with event's group with permission for user/member 1`] = `
"<div class="">
<!--v-if-->
<div class="flex flex-col gap-1 mt-1">
<p class="inline-flex gap-2 ml-auto">
<ticket-confirmation-outline-stub fillcolor="currentColor" size="24"></ticket-confirmation-outline-stub><a href="/events/f37910ea-fd5a-4756-9679-00971f3f4106/participations" class="participations-link underline">
<!-- We retire one because of the event creator who is a
participant --><span>No one is participating</span>
</a>
<!--v-if-->
</p>
<o-dropdown class="ml-auto">
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item>
<o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item>
<!--v-if-->
</o-dropdown>
</div>
</div>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close" autofocus="false" trapfocus="false">
<report-modal-stub title="Report this event"></report-modal-stub>
</o-modal>
<o-modal close-button-aria-label="Close" active="false" has-modal-card="">
<share-event-modal-stub event="[object Object]" eventcapacityok="true"></share-event-modal-stub>
</o-modal>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close">
<!--v-if-->
</o-modal>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Participation confirmation</p>
</header>
<section class="modal-card-body">
<p>The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?</p>
<form>
<o-field label="Message">
<o-input type="textarea" size="medium" modelvalue="" minlength="10"></o-input>
</o-field>
<div class="buttons">
<o-button native-type="button" class="button">Cancel</o-button>
<o-button variant="primary" type="submit">Confirm my participation</o-button>
</div>
</form>
</section>
</div>
</o-modal>"
`;
exports[`EventActionSection > event action section with event's group without permission for user/member 1`] = `
"<div class="">
<!--v-if-->
<div class="flex flex-col gap-1 mt-1">
<!--v-if-->
<o-dropdown class="ml-auto">
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item>
<o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item>
<!--v-if-->
</o-dropdown>
</div>
</div>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close" autofocus="false" trapfocus="false">
<report-modal-stub title="Report this event"></report-modal-stub>
</o-modal>
<o-modal close-button-aria-label="Close" active="false" has-modal-card="">
<share-event-modal-stub event="[object Object]" eventcapacityok="true"></share-event-modal-stub>
</o-modal>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close">
<!--v-if-->
</o-modal>
<o-modal active="false" has-modal-card="" close-button-aria-label="Close">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Participation confirmation</p>
</header>
<section class="modal-card-body">
<p>The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?</p>
<form>
<o-field label="Message">
<o-input type="textarea" size="medium" modelvalue="" minlength="10"></o-input>
</o-field>
<div class="buttons">
<o-button native-type="button" class="button">Cancel</o-button>
<o-button variant="primary" type="submit">Confirm my participation</o-button>
</div>
</form>
</section>
</div>
</o-modal>"
`;
exports[`EventActionSection > event action section with participant 1`] = ` exports[`EventActionSection > event action section with participant 1`] = `
"<div class=""> "<div class="">
<!--v-if--> <!--v-if-->
@@ -127,7 +223,6 @@ exports[`EventActionSection > event action section with participant 1`] = `
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if-->
<o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item> <o-dropdown-item aria-role="listitem" class="p-1"><span class="flex gap-1"><share-stub fillcolor="currentColor" size="24"></share-stub> Share this event</span></o-dropdown-item>
<o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item> <o-dropdown-item aria-role="listitem"><span class="flex gap-1"><calendar-plus-stub fillcolor="currentColor" size="24"></calendar-plus-stub> Add to my calendar</span></o-dropdown-item>
<!--v-if--> <!--v-if-->

View File

@@ -13,6 +13,7 @@ let mockClient: MockApolloClient | null;
export let requestHandlers: Record<string, RequestHandler>; export let requestHandlers: Record<string, RequestHandler>;
export function getMockClient(queries: Array<any>): any { export function getMockClient(queries: Array<any>): any {
cache.reset();
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,
resolvers: buildCurrentUserResolver(cache), resolvers: buildCurrentUserResolver(cache),