feat(webui): configure participant roster visibility

This commit is contained in:
2026-07-20 18:46:41 +02:00
parent 6141f39bf8
commit 705ac823ce
5 changed files with 78 additions and 2 deletions

View File

@@ -41,6 +41,8 @@ assert.match(createBranch, /<Card title=\{I18N\.calendarIntegration\}>/);
assert.match(page, /<Card title=\{I18N\.candidateSlots\}>/);
assert.match(page, /<Card title=\{I18N\.participants\}>/);
assert.match(createBranch, /<ToggleSwitch[\s\S]*checked=\{calendarEnabled\}/);
assert.match(page, /<ToggleSwitch[\s\S]*checked=\{participantRosterVisible\}/);
assert.match(page, /participant_visibility: participantRosterVisible \? "names_and_statuses" : "aggregates_only"/);
assert.match(page, /id="scheduling-create-candidate-slots-grid"/);
assert.match(page, /id="scheduling-create-participants-grid"/);
assert.match(page, /id="scheduling-candidate-slots-grid"/);
@@ -55,6 +57,10 @@ assert.match(page, /submitSchedulingAvailability\(settings, selected\.id/);
assert.match(page, /option_revision: slot\.revision/);
assert.match(page, /getSchedulingAvailabilityResponse\(settings, selected\.id\)/);
assert.match(page, /setAvailability\(Object\.fromEntries\(response\.answers\.map/);
assert.match(page, /selected\.participant_aggregate\.total/);
assert.match(page, /selected\.effective_participant_visibility === "names_and_statuses"/);
assert.match(api, /participant_visibility: SchedulingParticipantVisibility/);
assert.match(api, /participant_aggregate: SchedulingParticipantAggregate/);
assert.doesNotMatch(page, /<p>\{selected\.calendar_id\}<\/p>/);
assert.match(page, /className="scheduling-selected-calendar"/);
assert.match(page, /showPlanningCalendarActions/);

View File

@@ -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<string, unknown>;
};
export type SchedulingParticipantAggregate = {
total: number;
status_counts: Record<string, number>;
};
export type SchedulingParticipantVisibilityDecision = {
requested_visibility: SchedulingParticipantVisibility;
effective_visibility: SchedulingParticipantVisibility;
policy_applied: boolean;
reason?: string | null;
source_path: Record<string, unknown>[];
details: Record<string, unknown>;
};
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;

View File

@@ -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<SlotDraft[]>(() => initialSlots(translateText));
const [participants, setParticipants] = useState<ParticipantDraft[]>(() => [emptyParticipant()]);
const [participantRosterVisible, setParticipantRosterVisible] = useState(false);
const [availability, setAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
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
<section id={CREATE_SECTION_IDS.participants} className="scheduling-create-section">
<EditableParticipants
participants={participants}
participantRosterVisible={participantRosterVisible}
disabled={!canWrite}
onParticipantRosterVisibleChange={(checked) => {
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}>
<div className="scheduling-status-row">
<span className={`scheduling-status scheduling-status-${selected.status}`}>{requestStatusLabel(selected, actor)}</span>
<span>{i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participants.length })}</span>
<span>{i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participant_aggregate.total })}</span>
{summary ? <span>{i18nMessage("i18n:govoplan-scheduling.value_responses.ba17af9a", { value0: summary.poll_summary.response_count })}</span> : null}
</div>
{selected.description ? <p>{selected.description}</p> : null}
@@ -721,7 +731,17 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
<div className="scheduling-columns">
<Card title={I18N.participants}>
<div className="scheduling-status-row">
<span>{i18nMessage("i18n:govoplan-scheduling.value_participants.e776b092", { value0: selected.participant_aggregate.total })}</span>
{Object.entries(selected.participant_aggregate.status_counts)
.filter(([, count]) => count > 0)
.map(([status, count]) => (
<span key={status}><strong>{participantStatusLabel(status)}</strong> {count}</span>
))}
</div>
{selected.effective_participant_visibility === "names_and_statuses" ? (
<ParticipantsGrid participants={selected.participants} />
) : <p className="scheduling-capability-note">{I18N.participantRosterHidden}</p>}
</Card>
<Card title={I18N.notifications}>
{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<ParticipantDraft>) => void;
onMove: (from: number, to: number) => void;
@@ -988,6 +1013,12 @@ function EditableParticipants({
return (
<Card title={I18N.participants}>
<ToggleSwitch
label={I18N.participantRosterVisibility}
help={I18N.participantRosterVisibilityHelp}
checked={participantRosterVisible}
disabled={disabled}
onChange={onParticipantRosterVisibleChange} />
<DataGrid
id="scheduling-create-participants-grid"
rows={participants}

View File

@@ -59,6 +59,9 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.option_value.3f643dc0": "Option {value0}",
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Other scheduling requests",
"i18n:govoplan-scheduling.participant_email.2cadfd9e": "Participant email",
"i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a": "Participant names and statuses are hidden. Aggregate counts remain visible.",
"i18n:govoplan-scheduling.share_participant_names_and_response_statuses.df0bf9e0": "Share participant names and response statuses",
"i18n:govoplan-scheduling.when_enabled_participants_can_see_other_participants_names.3ac78361": "When enabled, participants can see other participants' names and response statuses. Email addresses and invitation details remain private.",
"i18n:govoplan-scheduling.participants.cd56e083": "Participants",
"i18n:govoplan-scheduling.past.405c12fb": "Past",
"i18n:govoplan-scheduling.pending.96f608c1": "Pending",
@@ -164,6 +167,9 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.option_value.3f643dc0": "Vorschlag {value0}",
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Andere Terminanfragen",
"i18n:govoplan-scheduling.participant_email.2cadfd9e": "E-Mail der teilnehmenden Person",
"i18n:govoplan-scheduling.participant_names_and_statuses_are_hidden_aggregate_counts_r.d811a69a": "Namen und Antwortstatus der Teilnehmenden sind ausgeblendet. Zusammengefasste Anzahlen bleiben sichtbar.",
"i18n:govoplan-scheduling.share_participant_names_and_response_statuses.df0bf9e0": "Namen und Antwortstatus der Teilnehmenden freigeben",
"i18n:govoplan-scheduling.when_enabled_participants_can_see_other_participants_names.3ac78361": "Wenn aktiviert, sehen Teilnehmende die Namen und Antwortstatus anderer Teilnehmender. E-Mail-Adressen und Einladungsdetails bleiben privat.",
"i18n:govoplan-scheduling.participants.cd56e083": "Teilnehmende",
"i18n:govoplan-scheduling.past.405c12fb": "Vergangen",
"i18n:govoplan-scheduling.pending.96f608c1": "Ausstehend",

View File

@@ -34,6 +34,19 @@ function request(
allow_external_participants: true,
allow_participant_updates: true,
result_visibility: "after_close",
participant_visibility: "aggregates_only",
effective_participant_visibility: "aggregates_only",
participant_aggregate: {
total: options.participantStatus ? 1 : 0,
status_counts: options.participantStatus ? { [options.participantStatus]: 1 } : {}
},
participant_visibility_decision: {
requested_visibility: "aggregates_only",
effective_visibility: "aggregates_only",
policy_applied: false,
source_path: [],
details: {}
},
calendar_integration_enabled: false,
calendar_freebusy_enabled: false,
calendar_hold_enabled: false,