diff --git a/webui/scripts/test-scheduling-page-structure.mjs b/webui/scripts/test-scheduling-page-structure.mjs
index 07e2b6d..3f44d88 100644
--- a/webui/scripts/test-scheduling-page-structure.mjs
+++ b/webui/scripts/test-scheduling-page-structure.mjs
@@ -52,10 +52,13 @@ assert.match(page, /type="email"[\s\S]*aria-label=\{I18N\.participantEmail\}/);
assert.doesNotMatch(page, /header: I18N\.required|
\{selected\.calendar_id\}<\/p>/);
assert.match(page, /className="scheduling-selected-calendar"/);
assert.match(page, /showPlanningCalendarActions/);
assert.match(page, /title=\{!canReadAvailability \? I18N\.requiresAvailabilityRead/);
assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses/);
+assert.match(api, /\/api\/v1\/scheduling\/requests\/\$\{requestId\}\/responses\/me/);
console.log("Scheduling page structure satisfies the focused list/create/respond contract.");
diff --git a/webui/src/api/scheduling.ts b/webui/src/api/scheduling.ts
index 2939731..fbe90c0 100644
--- a/webui/src/api/scheduling.ts
+++ b/webui/src/api/scheduling.ts
@@ -124,6 +124,17 @@ export type SchedulingAvailabilityPayload = {
}>;
};
+export type SchedulingAvailabilityResponse = {
+ request_id: string;
+ participant_id: string;
+ has_response: boolean;
+ submitted_at?: string | null;
+ answers: Array<{
+ slot_id: string;
+ value: SchedulingAvailabilityValue;
+ }>;
+};
+
export type SchedulingCalendarActionResponse = {
request: SchedulingRequest;
created_event_ids: string[];
@@ -218,6 +229,16 @@ export function submitSchedulingAvailability(
);
}
+export function getSchedulingAvailabilityResponse(
+ settings: ApiSettings,
+ requestId: string
+): Promise {
+ return apiFetch(
+ settings,
+ `/api/v1/scheduling/requests/${requestId}/responses/me`
+ );
+}
+
export function openSchedulingRequest(settings: ApiSettings, requestId: string): Promise {
return apiFetch(settings, `/api/v1/scheduling/requests/${requestId}/open`, json({}));
}
diff --git a/webui/src/features/scheduling/SchedulingPage.tsx b/webui/src/features/scheduling/SchedulingPage.tsx
index 9fd175d..6620667 100644
--- a/webui/src/features/scheduling/SchedulingPage.tsx
+++ b/webui/src/features/scheduling/SchedulingPage.tsx
@@ -36,6 +36,7 @@ import {
createSchedulingRequest,
decideSchedulingRequest,
evaluateSchedulingFreeBusy,
+ getSchedulingAvailabilityResponse,
listSchedulingNotifications,
listSchedulingRequests,
openSchedulingRequest,
@@ -176,6 +177,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
const [slots, setSlots] = useState(() => initialSlots(translateText));
const [participants, setParticipants] = useState(() => [emptyParticipant()]);
const [availability, setAvailability] = useState>({});
+ const [availabilityLoading, setAvailabilityLoading] = useState(false);
const canWrite = hasScope(auth, "scheduling:schedule:write");
const canRespond = hasScope(auth, "scheduling:availability:write");
@@ -240,6 +242,39 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
void loadDetails(selected.id);
}, [creating, selected?.id]);
+ useEffect(() => {
+ if (!selected?.id || !selectedParticipant || !canRespond || creating || selected.status !== "collecting") {
+ setAvailabilityLoading(false);
+ return undefined;
+ }
+ let cancelled = false;
+ setAvailabilityLoading(true);
+ void getSchedulingAvailabilityResponse(settings, selected.id)
+ .then((response) => {
+ if (cancelled) return;
+ setAvailability(Object.fromEntries(response.answers.map((answer) => [answer.slot_id, answer.value])));
+ })
+ .catch((err) => {
+ if (!cancelled) setError(errorMessage(err, translateText(I18N.requestFailed)));
+ })
+ .finally(() => {
+ if (!cancelled) setAvailabilityLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [
+ canRespond,
+ creating,
+ selected?.id,
+ selected?.status,
+ selectedParticipant?.id,
+ settings.accessToken,
+ settings.apiBaseUrl,
+ settings.apiKey,
+ translateText
+ ]);
+
useEffect(() => {
if (!creating || !draftDirty) return undefined;
const preventUnsavedExit = (event: BeforeUnloadEvent) => event.preventDefault();
@@ -366,7 +401,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
async function sendAvailability(event: FormEvent) {
event.preventDefault();
- if (!selected || !selectedParticipant || !canRespond) return;
+ if (!selected || !selectedParticipant || !canRespond || availabilityLoading) return;
const answers = selected.slots
.map((slot) => ({ slot_id: slot.id, value: availability[slot.id] }))
.filter((answer): answer is { slot_id: string; value: SchedulingAvailabilityValue } => Boolean(answer.value));
@@ -578,12 +613,16 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
variant="primary"
type="submit"
form="scheduling-response-form"
- disabled={saving || !canRespond || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}>
+ disabled={saving || availabilityLoading || !canRespond || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}>
{selectedParticipant.status === "responded" ? I18N.updateResponse : I18N.sendResponse}
)}>
-