From 705ac823ceeedb62c3b55976d29b286a679ae0a8 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 20 Jul 2026 18:46:41 +0200 Subject: [PATCH] feat(webui): configure participant roster visibility --- .../test-scheduling-page-structure.mjs | 6 ++++ webui/src/api/scheduling.ts | 20 +++++++++++ .../features/scheduling/SchedulingPage.tsx | 35 +++++++++++++++++-- webui/src/i18n/generatedTranslations.ts | 6 ++++ webui/tests/scheduling-view-model.test.ts | 13 +++++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/webui/scripts/test-scheduling-page-structure.mjs b/webui/scripts/test-scheduling-page-structure.mjs index c84bc66..b03fa18 100644 --- a/webui/scripts/test-scheduling-page-structure.mjs +++ b/webui/scripts/test-scheduling-page-structure.mjs @@ -41,6 +41,8 @@ assert.match(createBranch, //); assert.match(page, //); assert.match(page, //); assert.match(createBranch, /\{selected\.calendar_id\}<\/p>/); assert.match(page, /className="scheduling-selected-calendar"/); assert.match(page, /showPlanningCalendarActions/); diff --git a/webui/src/api/scheduling.ts b/webui/src/api/scheduling.ts index 7f2a1ce..ba33336 100644 --- a/webui/src/api/scheduling.ts +++ b/webui/src/api/scheduling.ts @@ -1,6 +1,7 @@ import { apiFetch, type ApiSettings } from "@govoplan/core-webui"; export type SchedulingStatus = "draft" | "collecting" | "closed" | "decided" | "handed_off" | "cancelled" | "archived"; +export type SchedulingParticipantVisibility = "aggregates_only" | "names_and_statuses"; export type SchedulingCandidateSlot = { id: string; @@ -35,6 +36,20 @@ export type SchedulingParticipant = { metadata?: Record; }; +export type SchedulingParticipantAggregate = { + total: number; + status_counts: Record; +}; + +export type SchedulingParticipantVisibilityDecision = { + requested_visibility: SchedulingParticipantVisibility; + effective_visibility: SchedulingParticipantVisibility; + policy_applied: boolean; + reason?: string | null; + source_path: Record[]; + details: Record; +}; + export type SchedulingRequest = { id: string; tenant_id: string; @@ -50,6 +65,10 @@ export type SchedulingRequest = { allow_external_participants: boolean; allow_participant_updates: boolean; result_visibility: string; + participant_visibility: SchedulingParticipantVisibility; + effective_participant_visibility: SchedulingParticipantVisibility; + participant_aggregate: SchedulingParticipantAggregate; + participant_visibility_decision: SchedulingParticipantVisibilityDecision; calendar_integration_enabled: boolean; calendar_id?: string | null; calendar_freebusy_enabled: boolean; @@ -97,6 +116,7 @@ export type SchedulingRequestCreatePayload = { allow_external_participants?: boolean; allow_participant_updates?: boolean; result_visibility?: "organizer" | "after_response" | "after_close" | "public"; + participant_visibility?: SchedulingParticipantVisibility; calendar?: { enabled?: boolean; calendar_id?: string | null; diff --git a/webui/src/features/scheduling/SchedulingPage.tsx b/webui/src/features/scheduling/SchedulingPage.tsx index 50ba6c9..4b51e6c 100644 --- a/webui/src/features/scheduling/SchedulingPage.tsx +++ b/webui/src/features/scheduling/SchedulingPage.tsx @@ -111,6 +111,9 @@ const I18N = { open: "i18n:govoplan-scheduling.open.cf9b7706", openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7", participantEmail: "i18n:govoplan-scheduling.participant_email.2cadfd9e", + participantRosterHidden: "i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a", + participantRosterVisibility: "i18n:govoplan-scheduling.share_participant_names_and_response_statuses.df0bf9e0", + participantRosterVisibilityHelp: "i18n:govoplan-scheduling.when_enabled_participants_can_see_other_participants_names.3ac78361", participants: "i18n:govoplan-scheduling.participants.cd56e083", past: "i18n:govoplan-scheduling.past.405c12fb", refresh: "i18n:govoplan-scheduling.refresh_requests.0a3ed7a1", @@ -176,6 +179,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin const [calendarEnabled, setCalendarEnabled] = useState(false); const [slots, setSlots] = useState(() => initialSlots(translateText)); const [participants, setParticipants] = useState(() => [emptyParticipant()]); + const [participantRosterVisible, setParticipantRosterVisible] = useState(false); const [availability, setAvailability] = useState>({}); const [availabilityLoading, setAvailabilityLoading] = useState(false); @@ -347,6 +351,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin location: location.trim() || null, timezone, status: "collecting", + participant_visibility: participantRosterVisible ? "names_and_statuses" : "aggregates_only", calendar: { enabled: calendarEnabled, calendar_id: calendarEnabled ? calendarId : null, @@ -534,7 +539,12 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
{ + setParticipantRosterVisible(checked); + setDraftDirty(true); + }} onAddBelow={(index) => { setParticipants((items) => insertAfter(items, index, emptyParticipant())); setDraftDirty(true); @@ -606,7 +616,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin ) : null}>
{requestStatusLabel(selected, actor)} - {i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participants.length })} + {i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participant_aggregate.total })} {summary ? {i18nMessage("i18n:govoplan-scheduling.value_responses.ba17af9a", { value0: summary.poll_summary.response_count })} : null}
{selected.description ?

{selected.description}

: null} @@ -721,7 +731,17 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
- +
+ {i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participant_aggregate.total })} + {Object.entries(selected.participant_aggregate.status_counts) + .filter(([, count]) => count > 0) + .map(([status, count]) => ( + {participantStatusLabel(status)} {count} + ))} +
+ {selected.effective_participant_visibility === "names_and_statuses" ? ( + + ) :

{I18N.participantRosterHidden}

}
{notifications.length ? notifications.map((notification) => ( @@ -767,6 +787,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin setCalendarEnabled(false); setSlots(initialSlots(translateText)); setParticipants([emptyParticipant()]); + setParticipantRosterVisible(false); setCreateSection("basic"); setDraftDirty(false); } @@ -923,14 +944,18 @@ function EditableSlots({ function EditableParticipants({ participants, + participantRosterVisible, disabled, + onParticipantRosterVisibleChange, onAddBelow, onChange, onMove, onRemove }: { participants: ParticipantDraft[]; + participantRosterVisible: boolean; disabled: boolean; + onParticipantRosterVisibleChange: (checked: boolean) => void; onAddBelow: (index: number) => void; onChange: (index: number, patch: Partial) => void; onMove: (from: number, to: number) => void; @@ -988,6 +1013,12 @@ function EditableParticipants({ return ( +