feat: reconcile tentative calendar holds
This commit is contained in:
@@ -39,7 +39,7 @@ const editorStart = page.indexOf('<div className="scheduling-editor-surface">');
|
||||
const editorEnd = page.indexOf('</form>', editorStart);
|
||||
const editor = page.slice(editorStart, editorEnd);
|
||||
assert.ok(editor.indexOf("I18N.discard") < editor.indexOf("I18N.save"));
|
||||
assert.match(editor, /form="scheduling-editor-form"/);
|
||||
assert.match(editor, /form:\s*"scheduling-editor-form"/);
|
||||
assert.match(editor, /<Card title=\{I18N\.basicInformation\}>/);
|
||||
assert.match(editor, /<Card title=\{I18N\.calendarIntegration\}>/);
|
||||
assert.match(page, /<Card title=\{I18N\.candidateSlots\}>/);
|
||||
@@ -55,6 +55,7 @@ assert.match(editor, /<FormField label=\{I18N\.title\}>/);
|
||||
assert.match(page, /useUnsavedDraftGuard\(/);
|
||||
assert.match(page, /requestDiscard\(exitEditor\)/);
|
||||
assert.match(page, /dirty: responseDirty,[\s\S]*onSave: persistAvailability,[\s\S]*onDiscard: resetResponseDraft/);
|
||||
assert.match(page, /calendarCleanup\?\.status === "retry_required"[\s\S]*I18N\.calendarCleanupRetryTitle/);
|
||||
assert.doesNotMatch(page, /window\.(?:alert|confirm)\(/);
|
||||
|
||||
for (const setting of [
|
||||
@@ -92,7 +93,7 @@ assert.match(page, /search=\{participantSearch\}/);
|
||||
assert.doesNotMatch(page, /<table|scheduling-table|scheduling-card(?:\s|"|`)/);
|
||||
assert.match(page, /<TableActionGroup[\s\S]*disabled: saving \|\| !decisionEnabled/);
|
||||
assert.match(page, /showDecisionAction=\{canManageSelected\}/);
|
||||
assert.match(page, /<IconButton[\s\S]*label=\{I18N\.refresh\}/);
|
||||
assert.match(page, /<WorkspaceActionBar[\s\S]*refreshable[\s\S]*reloadAction=\{\{[\s\S]*label: I18N\.refresh/);
|
||||
assert.doesNotMatch(page, /AdminIconButton/);
|
||||
|
||||
const participantGridStart = page.indexOf("function ParticipantsGrid(");
|
||||
|
||||
@@ -27,7 +27,6 @@ import { ContentGrid, FormGrid,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
MetricCard,
|
||||
IconButton,
|
||||
PageTitle,
|
||||
PasswordField,
|
||||
PeoplePicker,
|
||||
@@ -127,6 +126,11 @@ type DecisionTarget = {
|
||||
requestId: string;
|
||||
slot: SchedulingCandidateSlot;
|
||||
};
|
||||
type SchedulingCalendarCleanup = {
|
||||
status: "accepted" | "retry_required";
|
||||
targetState: string | null;
|
||||
pendingCount: number;
|
||||
};
|
||||
|
||||
const I18N = {
|
||||
actions: "i18n:govoplan-core.actions.c3cd636a",
|
||||
@@ -148,6 +152,8 @@ const I18N = {
|
||||
calendarIntegration: "i18n:govoplan-scheduling.calendar_integration.181ad18b",
|
||||
calendarUnavailable: "i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e",
|
||||
calendarRequiredAction: "i18n:govoplan-scheduling.enable_calendar_and_grant_calendar_availability_and_event_access.f1a20106",
|
||||
calendarCleanupRetryTitle: "i18n:govoplan-scheduling.calendar_cleanup_retry_title",
|
||||
calendarCleanupRetryMessage: "i18n:govoplan-scheduling.calendar_cleanup_retry_message",
|
||||
candidateAvailability: "i18n:govoplan-scheduling.candidate_availability.9541c4b5",
|
||||
candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
|
||||
cancellationNoticeExpired: "i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c",
|
||||
@@ -342,6 +348,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
() => requests.find((item) => item.id === selectedId) ?? firstRequest(groups, canAdminister),
|
||||
[canAdminister, groups, requests, selectedId]
|
||||
);
|
||||
const calendarCleanup = selected ? schedulingCalendarCleanup(selected) : null;
|
||||
const selectedParticipant = useMemo(
|
||||
() => selected ? schedulingParticipantForActor(selected, actor) : null,
|
||||
[actor, selected]
|
||||
@@ -368,7 +375,10 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
selectedPhase !== "past" &&
|
||||
selected.status === "decided" &&
|
||||
selected.selected_slot_id &&
|
||||
!selected.calendar_event_id &&
|
||||
(!selected.calendar_event_id || (
|
||||
calendarCleanup?.status === "retry_required"
|
||||
&& calendarCleanup.targetState === "handed_off"
|
||||
)) &&
|
||||
selected.create_calendar_event_on_decision
|
||||
);
|
||||
const optionResultById = useMemo(() => {
|
||||
@@ -1298,7 +1308,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
onDecide={(slot) => setDecisionTarget({ requestId: selected.id, slot })} />
|
||||
</Card>
|
||||
|
||||
{selected.calendar_integration_enabled && canManageSelected && (showPlanningCalendarActions || showFinalCalendarAction || selected.calendar_event_id) ? (
|
||||
{selected.calendar_integration_enabled && canManageSelected && (showPlanningCalendarActions || showFinalCalendarAction || selected.calendar_event_id || calendarCleanup?.status === "retry_required") ? (
|
||||
<Card
|
||||
title={I18N.calendarCoordination}
|
||||
actions={(
|
||||
@@ -1342,6 +1352,14 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
label={I18N.configuredCalendar}
|
||||
disabled />
|
||||
) : <p>{I18N.configuredCalendar}</p>}
|
||||
{calendarCleanup?.status === "retry_required" ? (
|
||||
<DismissibleAlert tone="warning" dismissible={false} compact>
|
||||
<strong>{I18N.calendarCleanupRetryTitle}</strong>{" "}
|
||||
{i18nMessage(I18N.calendarCleanupRetryMessage, {
|
||||
value0: calendarCleanup.pendingCount
|
||||
})}
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
@@ -1714,6 +1732,20 @@ function ParticipationStats({
|
||||
);
|
||||
}
|
||||
|
||||
function schedulingCalendarCleanup(request: SchedulingRequest): SchedulingCalendarCleanup | null {
|
||||
const value = request.metadata?.calendar_cleanup;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
const status = record.status === "retry_required" ? "retry_required" : record.status === "accepted" ? "accepted" : null;
|
||||
if (!status) return null;
|
||||
const pending = Array.isArray(record.pending_slot_ids) ? record.pending_slot_ids : [];
|
||||
return {
|
||||
status,
|
||||
targetState: typeof record.target_state === "string" ? record.target_state : null,
|
||||
pendingCount: pending.length
|
||||
};
|
||||
}
|
||||
|
||||
function schedulingActionConfirmation(kind: ConsequentialAction["kind"]): {
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const generatedTranslations = {
|
||||
en: {
|
||||
"i18n:govoplan-scheduling.calendar_cleanup_retry_title": "Calendar cleanup requires attention.",
|
||||
"i18n:govoplan-scheduling.calendar_cleanup_retry_message": "{value0} tentative hold operations remain. Reconcile failed Calendar outbound changes if necessary, then repeat the original decision or cancellation action.",
|
||||
"i18n:govoplan-scheduling.access_details.79c06b89": "Access details",
|
||||
"i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3": "Automatic invitation delivery is unavailable; copy the link instead.",
|
||||
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Cancellation notice available until",
|
||||
@@ -180,6 +182,8 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Your response has been recorded."
|
||||
},
|
||||
de: {
|
||||
"i18n:govoplan-scheduling.calendar_cleanup_retry_title": "Die Kalenderbereinigung erfordert Aufmerksamkeit.",
|
||||
"i18n:govoplan-scheduling.calendar_cleanup_retry_message": "{value0} Vorgänge für vorläufige Reservierungen stehen noch aus. Gleichen Sie fehlgeschlagene ausgehende Kalenderänderungen bei Bedarf ab und wiederholen Sie anschließend die ursprüngliche Entscheidungs- oder Abbruchaktion.",
|
||||
"i18n:govoplan-scheduling.access_details.79c06b89": "Zugangsdaten",
|
||||
"i18n:govoplan-scheduling.automatic_invitation_delivery_is_unavailable_copy_the_link_instead.4e39d0b3": "Die automatische Einladungszustellung ist nicht verfügbar; kopieren Sie stattdessen den Link.",
|
||||
"i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6": "Stornierungshinweis verfügbar bis",
|
||||
|
||||
Reference in New Issue
Block a user