Skip to content

Commit 9f56c27

Browse files
adrians5jclaude
andauthored
fix(app-scheduler): show a failed schedule load instead of an empty dialog (6.5.0) (#5995)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2c24005 commit 9f56c27

4 files changed

Lines changed: 53 additions & 4 deletions

File tree

‎packages/app-scheduler/src/presentation/scheduleDialog/ScheduleDialogPresenter.test.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ScheduleDialogPresenter } from "./ScheduleDialogPresenter.js";
1515
* `cancel()` resolves, and shows the error when they reject. So a failed action must reject,
1616
* and the dialog must not be left loading.
1717
*/
18-
const setup = (params: { fail: boolean }) => {
18+
const setup = (params: { fail: boolean; getExecute?: () => Promise<unknown> }) => {
1919
const execute = vi.fn(async () => {
2020
if (params.fail) {
2121
throw new Error("Could not schedule the action.");
@@ -24,7 +24,9 @@ const setup = (params: { fail: boolean }) => {
2424
});
2525

2626
const container = new Container();
27-
container.registerInstance(GetScheduledActionGateway, { execute: vi.fn() } as any);
27+
container.registerInstance(GetScheduledActionGateway, {
28+
execute: params.getExecute ?? vi.fn()
29+
} as any);
2830
container.registerInstance(CancelScheduledActionGateway, { execute } as any);
2931
container.registerInstance(SchedulePublishActionGateway, { execute } as any);
3032
container.registerInstance(ScheduleUnpublishActionGateway, { execute } as any);
@@ -74,4 +76,33 @@ describe("ScheduleDialogPresenter", () => {
7476
).resolves.toBeUndefined();
7577
expect(presenter.vm.loading).toBe(false);
7678
});
79+
80+
/*
81+
* The dialog loads the existing schedule before it opens. A failed load used to leave the
82+
* entry empty, which reads as "nothing scheduled" and invites scheduling a duplicate.
83+
*/
84+
it("rejects when loading the scheduled action fails", async () => {
85+
const presenter = setup({
86+
fail: false,
87+
getExecute: async () => {
88+
throw new Error("Could not fetch scheduled action.");
89+
}
90+
});
91+
92+
await expect(presenter.load({ namespace: "cms/article", id: "entry-1" })).rejects.toThrow(
93+
"Could not fetch scheduled action."
94+
);
95+
expect(presenter.vm.loading).toBe(false);
96+
expect(presenter.vm.entry).toBeNull();
97+
});
98+
99+
it("resolves with no entry when nothing is scheduled", async () => {
100+
const presenter = setup({ fail: false, getExecute: async () => null });
101+
102+
await expect(
103+
presenter.load({ namespace: "cms/article", id: "entry-1" })
104+
).resolves.toBeUndefined();
105+
expect(presenter.vm.loading).toBe(false);
106+
expect(presenter.vm.entry).toBeNull();
107+
});
77108
});

‎packages/app-scheduler/src/presentation/scheduleDialog/ScheduleDialogPresenter.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ class ScheduleDialogPresenterImpl implements IScheduleDialogPresenter {
6565
this.entry = entry;
6666
this.loading = false;
6767
});
68-
} catch {
68+
} catch (error) {
6969
runInAction(() => {
7070
this.loading = false;
7171
});
72+
throw error;
7273
}
7374
}
7475

‎packages/app-scheduler/src/presentation/scheduleDialog/abstractions.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface IScheduleDialogPresenterCancelParams {
3131

3232
export interface IScheduleDialogPresenter {
3333
get vm(): IScheduleDialogPresenterViewModel;
34+
/**
35+
* Rejects when loading fails. Resolves with `vm.entry` set to null when nothing is scheduled, so
36+
* the two can't be confused.
37+
*/
3438
load(params: IScheduleDialogPresenterLoadParams): Promise<void>;
3539
/** Rejects when scheduling fails, so the caller can show the error. */
3640
schedule(params: IScheduleDialogPresenterScheduleParams): Promise<void>;

‎packages/app-scheduler/src/presentation/scheduleDialog/useScheduleDialog.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,20 @@ export const useScheduleDialog = (
114114
title: `Schedule "${target.title}"`,
115115
content: <FormComponent presenter={presenter} />,
116116
formData: async () => {
117-
await presenter.load({ namespace, id: target.id });
117+
try {
118+
await presenter.load({ namespace, id: target.id });
119+
} catch (error) {
120+
/*
121+
* Without the current schedule the dialog can't tell a new action from a
122+
* duplicate, so show the error and close it instead of offering the form.
123+
*/
124+
toast.showWarningToast({ title: error.message });
125+
if (dialogClose.current) {
126+
dialogClose.current();
127+
dialogClose.current = null;
128+
}
129+
return {};
130+
}
118131

119132
const entry = presenter.vm.entry;
120133
const scheduleOn = entry?.publishOn || entry?.unpublishOn;

0 commit comments

Comments
 (0)