feat(scheduling-webui): model invitation actions
This commit is contained in:
@@ -84,6 +84,7 @@ export type SchedulingRequest = {
|
||||
anonymous_password_protection_enabled: boolean;
|
||||
public_participation_policy_enforcement_available: boolean | null;
|
||||
public_participation_policy_enforcement_reason?: string | null;
|
||||
participant_invitation_delivery_available: boolean | null;
|
||||
calendar_integration_enabled: boolean | null;
|
||||
calendar_id?: string | null;
|
||||
calendar_freebusy_enabled: boolean | null;
|
||||
@@ -288,6 +289,18 @@ export type SchedulingNotification = {
|
||||
|
||||
export type SchedulingNotificationListResponse = { notifications: SchedulingNotification[] };
|
||||
|
||||
export type SchedulingInvitationAction = "copy" | "send" | "revoke";
|
||||
|
||||
export type SchedulingInvitationActionResponse = {
|
||||
participant_id: string;
|
||||
action: SchedulingInvitationAction;
|
||||
status: string;
|
||||
action_url?: string | null;
|
||||
issued_at?: string | null;
|
||||
replayed: boolean;
|
||||
notification?: SchedulingNotification | null;
|
||||
};
|
||||
|
||||
export type SchedulingPollOptionResult = {
|
||||
option_id: string;
|
||||
option_key: string;
|
||||
@@ -446,3 +459,30 @@ export function listSchedulingNotifications(settings: ApiSettings, requestId?: s
|
||||
const query = requestId ? `?request_id=${encodeURIComponent(requestId)}` : "";
|
||||
return apiFetch<SchedulingNotificationListResponse>(settings, `/api/v1/scheduling/notifications${query}`);
|
||||
}
|
||||
|
||||
export function issueSchedulingParticipantInvitation(
|
||||
settings: ApiSettings,
|
||||
requestId: string,
|
||||
participantId: string,
|
||||
participantRevision: string,
|
||||
action: Exclude<SchedulingInvitationAction, "revoke">
|
||||
): Promise<SchedulingInvitationActionResponse> {
|
||||
return apiFetch<SchedulingInvitationActionResponse>(
|
||||
settings,
|
||||
`/api/v1/scheduling/requests/${encodeURIComponent(requestId)}/participants/${encodeURIComponent(participantId)}/invitation`,
|
||||
json({ action, participant_revision: participantRevision })
|
||||
);
|
||||
}
|
||||
|
||||
export function revokeSchedulingParticipantInvitation(
|
||||
settings: ApiSettings,
|
||||
requestId: string,
|
||||
participantId: string,
|
||||
participantRevision: string
|
||||
): Promise<SchedulingInvitationActionResponse> {
|
||||
return apiFetch<SchedulingInvitationActionResponse>(
|
||||
settings,
|
||||
`/api/v1/scheduling/requests/${encodeURIComponent(requestId)}/participants/${encodeURIComponent(participantId)}/invitation`,
|
||||
{ method: "DELETE", body: JSON.stringify({ participant_revision: participantRevision }) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,20 @@ export type SchedulingSortPhase =
|
||||
| "determined"
|
||||
| "past";
|
||||
|
||||
export type SchedulingInvitationActionBlock =
|
||||
| "participation_policy_unavailable"
|
||||
| "cancellation_notice_expired"
|
||||
| "delivery_unavailable"
|
||||
| "no_delivery_target"
|
||||
| "no_active_invitation"
|
||||
| "participant_revision_unavailable";
|
||||
|
||||
export type SchedulingInvitationActionBlocks = {
|
||||
copy: SchedulingInvitationActionBlock | null;
|
||||
send: SchedulingInvitationActionBlock | null;
|
||||
revoke: SchedulingInvitationActionBlock | null;
|
||||
};
|
||||
|
||||
type DirectorySelection = {
|
||||
selection_key?: string;
|
||||
kind?: PeoplePickerItem["kind"];
|
||||
@@ -268,6 +282,65 @@ export function schedulingRequestIsPast(
|
||||
return slotEnds.every((value) => Number.isFinite(value) && value < now.getTime());
|
||||
}
|
||||
|
||||
export function schedulingInvitationActionBlocks(
|
||||
request: SchedulingRequest,
|
||||
participant: SchedulingParticipant,
|
||||
now = new Date()
|
||||
): SchedulingInvitationActionBlocks {
|
||||
if (!participant.revision) {
|
||||
return {
|
||||
copy: "participant_revision_unavailable",
|
||||
send: "participant_revision_unavailable",
|
||||
revoke: "participant_revision_unavailable"
|
||||
};
|
||||
}
|
||||
const policyAvailable = Boolean(
|
||||
request.poll_id &&
|
||||
request.public_participation_policy_enforcement_available === true
|
||||
);
|
||||
let issueBlock: SchedulingInvitationActionBlock | null = policyAvailable
|
||||
? null
|
||||
: "participation_policy_unavailable";
|
||||
if (!issueBlock && request.status === "cancelled") {
|
||||
const noticeUntil = request.cancellation_notice_until
|
||||
? Date.parse(request.cancellation_notice_until)
|
||||
: Number.NaN;
|
||||
if (!Number.isFinite(noticeUntil) || noticeUntil <= now.getTime()) {
|
||||
issueBlock = "cancellation_notice_expired";
|
||||
}
|
||||
}
|
||||
|
||||
const respondentId = participant.respondent_id?.trim() ?? "";
|
||||
const hasDeliveryTarget = Boolean(
|
||||
participant.email?.trim() ||
|
||||
(respondentId && !respondentId.startsWith("scheduling-participant:"))
|
||||
);
|
||||
const sendBlock = issueBlock
|
||||
?? (request.participant_invitation_delivery_available === true
|
||||
? null
|
||||
: "delivery_unavailable")
|
||||
?? (hasDeliveryTarget ? null : "no_delivery_target");
|
||||
const revokeBlock = participant.poll_invitation_id
|
||||
? (policyAvailable ? null : "participation_policy_unavailable")
|
||||
: "no_active_invitation";
|
||||
|
||||
return { copy: issueBlock, send: sendBlock, revoke: revokeBlock };
|
||||
}
|
||||
|
||||
export function schedulingPublicInvitationUrl(
|
||||
actionUrl: string,
|
||||
applicationOrigin: string
|
||||
): string | null {
|
||||
try {
|
||||
const origin = new URL(applicationOrigin);
|
||||
const url = new URL(actionUrl, origin);
|
||||
if (url.origin !== origin.origin || !url.pathname.startsWith("/scheduling/public/")) return null;
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function applySchedulingAvailabilityChoice(
|
||||
slotIds: string[],
|
||||
current: Record<string, SchedulingAvailabilityValue | "">,
|
||||
|
||||
Reference in New Issue
Block a user