feat(webui): configure participant roster visibility
This commit is contained in:
@@ -41,6 +41,8 @@ assert.match(createBranch, /<Card title=\{I18N\.calendarIntegration\}>/);
|
|||||||
assert.match(page, /<Card title=\{I18N\.candidateSlots\}>/);
|
assert.match(page, /<Card title=\{I18N\.candidateSlots\}>/);
|
||||||
assert.match(page, /<Card title=\{I18N\.participants\}>/);
|
assert.match(page, /<Card title=\{I18N\.participants\}>/);
|
||||||
assert.match(createBranch, /<ToggleSwitch[\s\S]*checked=\{calendarEnabled\}/);
|
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-candidate-slots-grid"/);
|
||||||
assert.match(page, /id="scheduling-create-participants-grid"/);
|
assert.match(page, /id="scheduling-create-participants-grid"/);
|
||||||
assert.match(page, /id="scheduling-candidate-slots-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, /option_revision: slot\.revision/);
|
||||||
assert.match(page, /getSchedulingAvailabilityResponse\(settings, selected\.id\)/);
|
assert.match(page, /getSchedulingAvailabilityResponse\(settings, selected\.id\)/);
|
||||||
assert.match(page, /setAvailability\(Object\.fromEntries\(response\.answers\.map/);
|
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.doesNotMatch(page, /<p>\{selected\.calendar_id\}<\/p>/);
|
||||||
assert.match(page, /className="scheduling-selected-calendar"/);
|
assert.match(page, /className="scheduling-selected-calendar"/);
|
||||||
assert.match(page, /showPlanningCalendarActions/);
|
assert.match(page, /showPlanningCalendarActions/);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type SchedulingStatus = "draft" | "collecting" | "closed" | "decided" | "handed_off" | "cancelled" | "archived";
|
export type SchedulingStatus = "draft" | "collecting" | "closed" | "decided" | "handed_off" | "cancelled" | "archived";
|
||||||
|
export type SchedulingParticipantVisibility = "aggregates_only" | "names_and_statuses";
|
||||||
|
|
||||||
export type SchedulingCandidateSlot = {
|
export type SchedulingCandidateSlot = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -35,6 +36,20 @@ export type SchedulingParticipant = {
|
|||||||
metadata?: Record<string, unknown>;
|
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 = {
|
export type SchedulingRequest = {
|
||||||
id: string;
|
id: string;
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
@@ -50,6 +65,10 @@ export type SchedulingRequest = {
|
|||||||
allow_external_participants: boolean;
|
allow_external_participants: boolean;
|
||||||
allow_participant_updates: boolean;
|
allow_participant_updates: boolean;
|
||||||
result_visibility: string;
|
result_visibility: string;
|
||||||
|
participant_visibility: SchedulingParticipantVisibility;
|
||||||
|
effective_participant_visibility: SchedulingParticipantVisibility;
|
||||||
|
participant_aggregate: SchedulingParticipantAggregate;
|
||||||
|
participant_visibility_decision: SchedulingParticipantVisibilityDecision;
|
||||||
calendar_integration_enabled: boolean;
|
calendar_integration_enabled: boolean;
|
||||||
calendar_id?: string | null;
|
calendar_id?: string | null;
|
||||||
calendar_freebusy_enabled: boolean;
|
calendar_freebusy_enabled: boolean;
|
||||||
@@ -97,6 +116,7 @@ export type SchedulingRequestCreatePayload = {
|
|||||||
allow_external_participants?: boolean;
|
allow_external_participants?: boolean;
|
||||||
allow_participant_updates?: boolean;
|
allow_participant_updates?: boolean;
|
||||||
result_visibility?: "organizer" | "after_response" | "after_close" | "public";
|
result_visibility?: "organizer" | "after_response" | "after_close" | "public";
|
||||||
|
participant_visibility?: SchedulingParticipantVisibility;
|
||||||
calendar?: {
|
calendar?: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
calendar_id?: string | null;
|
calendar_id?: string | null;
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ const I18N = {
|
|||||||
open: "i18n:govoplan-scheduling.open.cf9b7706",
|
open: "i18n:govoplan-scheduling.open.cf9b7706",
|
||||||
openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7",
|
openPoll: "i18n:govoplan-scheduling.open_poll.2beac9a7",
|
||||||
participantEmail: "i18n:govoplan-scheduling.participant_email.2cadfd9e",
|
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",
|
participants: "i18n:govoplan-scheduling.participants.cd56e083",
|
||||||
past: "i18n:govoplan-scheduling.past.405c12fb",
|
past: "i18n:govoplan-scheduling.past.405c12fb",
|
||||||
refresh: "i18n:govoplan-scheduling.refresh_requests.0a3ed7a1",
|
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 [calendarEnabled, setCalendarEnabled] = useState(false);
|
||||||
const [slots, setSlots] = useState<SlotDraft[]>(() => initialSlots(translateText));
|
const [slots, setSlots] = useState<SlotDraft[]>(() => initialSlots(translateText));
|
||||||
const [participants, setParticipants] = useState<ParticipantDraft[]>(() => [emptyParticipant()]);
|
const [participants, setParticipants] = useState<ParticipantDraft[]>(() => [emptyParticipant()]);
|
||||||
|
const [participantRosterVisible, setParticipantRosterVisible] = useState(false);
|
||||||
const [availability, setAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
|
const [availability, setAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
|
||||||
const [availabilityLoading, setAvailabilityLoading] = useState(false);
|
const [availabilityLoading, setAvailabilityLoading] = useState(false);
|
||||||
|
|
||||||
@@ -347,6 +351,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
location: location.trim() || null,
|
location: location.trim() || null,
|
||||||
timezone,
|
timezone,
|
||||||
status: "collecting",
|
status: "collecting",
|
||||||
|
participant_visibility: participantRosterVisible ? "names_and_statuses" : "aggregates_only",
|
||||||
calendar: {
|
calendar: {
|
||||||
enabled: calendarEnabled,
|
enabled: calendarEnabled,
|
||||||
calendar_id: calendarEnabled ? calendarId : null,
|
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">
|
<section id={CREATE_SECTION_IDS.participants} className="scheduling-create-section">
|
||||||
<EditableParticipants
|
<EditableParticipants
|
||||||
participants={participants}
|
participants={participants}
|
||||||
|
participantRosterVisible={participantRosterVisible}
|
||||||
disabled={!canWrite}
|
disabled={!canWrite}
|
||||||
|
onParticipantRosterVisibleChange={(checked) => {
|
||||||
|
setParticipantRosterVisible(checked);
|
||||||
|
setDraftDirty(true);
|
||||||
|
}}
|
||||||
onAddBelow={(index) => {
|
onAddBelow={(index) => {
|
||||||
setParticipants((items) => insertAfter(items, index, emptyParticipant()));
|
setParticipants((items) => insertAfter(items, index, emptyParticipant()));
|
||||||
setDraftDirty(true);
|
setDraftDirty(true);
|
||||||
@@ -606,7 +616,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
) : null}>
|
) : null}>
|
||||||
<div className="scheduling-status-row">
|
<div className="scheduling-status-row">
|
||||||
<span className={`scheduling-status scheduling-status-${selected.status}`}>{requestStatusLabel(selected, actor)}</span>
|
<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}
|
{summary ? <span>{i18nMessage("i18n:govoplan-scheduling.value_responses.ba17af9a", { value0: summary.poll_summary.response_count })}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
{selected.description ? <p>{selected.description}</p> : null}
|
{selected.description ? <p>{selected.description}</p> : null}
|
||||||
@@ -721,7 +731,17 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
|
|
||||||
<div className="scheduling-columns">
|
<div className="scheduling-columns">
|
||||||
<Card title={I18N.participants}>
|
<Card title={I18N.participants}>
|
||||||
<ParticipantsGrid participants={selected.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>
|
||||||
<Card title={I18N.notifications}>
|
<Card title={I18N.notifications}>
|
||||||
{notifications.length ? notifications.map((notification) => (
|
{notifications.length ? notifications.map((notification) => (
|
||||||
@@ -767,6 +787,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
|||||||
setCalendarEnabled(false);
|
setCalendarEnabled(false);
|
||||||
setSlots(initialSlots(translateText));
|
setSlots(initialSlots(translateText));
|
||||||
setParticipants([emptyParticipant()]);
|
setParticipants([emptyParticipant()]);
|
||||||
|
setParticipantRosterVisible(false);
|
||||||
setCreateSection("basic");
|
setCreateSection("basic");
|
||||||
setDraftDirty(false);
|
setDraftDirty(false);
|
||||||
}
|
}
|
||||||
@@ -923,14 +944,18 @@ function EditableSlots({
|
|||||||
|
|
||||||
function EditableParticipants({
|
function EditableParticipants({
|
||||||
participants,
|
participants,
|
||||||
|
participantRosterVisible,
|
||||||
disabled,
|
disabled,
|
||||||
|
onParticipantRosterVisibleChange,
|
||||||
onAddBelow,
|
onAddBelow,
|
||||||
onChange,
|
onChange,
|
||||||
onMove,
|
onMove,
|
||||||
onRemove
|
onRemove
|
||||||
}: {
|
}: {
|
||||||
participants: ParticipantDraft[];
|
participants: ParticipantDraft[];
|
||||||
|
participantRosterVisible: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
onParticipantRosterVisibleChange: (checked: boolean) => void;
|
||||||
onAddBelow: (index: number) => void;
|
onAddBelow: (index: number) => void;
|
||||||
onChange: (index: number, patch: Partial<ParticipantDraft>) => void;
|
onChange: (index: number, patch: Partial<ParticipantDraft>) => void;
|
||||||
onMove: (from: number, to: number) => void;
|
onMove: (from: number, to: number) => void;
|
||||||
@@ -988,6 +1013,12 @@ function EditableParticipants({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={I18N.participants}>
|
<Card title={I18N.participants}>
|
||||||
|
<ToggleSwitch
|
||||||
|
label={I18N.participantRosterVisibility}
|
||||||
|
help={I18N.participantRosterVisibilityHelp}
|
||||||
|
checked={participantRosterVisible}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onParticipantRosterVisibleChange} />
|
||||||
<DataGrid
|
<DataGrid
|
||||||
id="scheduling-create-participants-grid"
|
id="scheduling-create-participants-grid"
|
||||||
rows={participants}
|
rows={participants}
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export const generatedTranslations = {
|
|||||||
"i18n:govoplan-scheduling.option_value.3f643dc0": "Option {value0}",
|
"i18n:govoplan-scheduling.option_value.3f643dc0": "Option {value0}",
|
||||||
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Other scheduling requests",
|
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Other scheduling requests",
|
||||||
"i18n:govoplan-scheduling.participant_email.2cadfd9e": "Participant email",
|
"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.participants.cd56e083": "Participants",
|
||||||
"i18n:govoplan-scheduling.past.405c12fb": "Past",
|
"i18n:govoplan-scheduling.past.405c12fb": "Past",
|
||||||
"i18n:govoplan-scheduling.pending.96f608c1": "Pending",
|
"i18n:govoplan-scheduling.pending.96f608c1": "Pending",
|
||||||
@@ -164,6 +167,9 @@ export const generatedTranslations = {
|
|||||||
"i18n:govoplan-scheduling.option_value.3f643dc0": "Vorschlag {value0}",
|
"i18n:govoplan-scheduling.option_value.3f643dc0": "Vorschlag {value0}",
|
||||||
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Andere Terminanfragen",
|
"i18n:govoplan-scheduling.other_scheduling_requests.5cb6eb30": "Andere Terminanfragen",
|
||||||
"i18n:govoplan-scheduling.participant_email.2cadfd9e": "E-Mail der teilnehmenden Person",
|
"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.participants.cd56e083": "Teilnehmende",
|
||||||
"i18n:govoplan-scheduling.past.405c12fb": "Vergangen",
|
"i18n:govoplan-scheduling.past.405c12fb": "Vergangen",
|
||||||
"i18n:govoplan-scheduling.pending.96f608c1": "Ausstehend",
|
"i18n:govoplan-scheduling.pending.96f608c1": "Ausstehend",
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ function request(
|
|||||||
allow_external_participants: true,
|
allow_external_participants: true,
|
||||||
allow_participant_updates: true,
|
allow_participant_updates: true,
|
||||||
result_visibility: "after_close",
|
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_integration_enabled: false,
|
||||||
calendar_freebusy_enabled: false,
|
calendar_freebusy_enabled: false,
|
||||||
calendar_hold_enabled: false,
|
calendar_hold_enabled: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user