mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
Fix phantom dirty banner after restoring a routine revision (PAP-3588)
After a successful revision restore, RoutineDetail's editor draft was not re-hydrated because routine.id is unchanged and isEditDirty became true relative to the freshly- restored routine, blocking the existing useEffect rehydration. Add an onRestored callback wired from RoutineHistoryTab so the parent can synchronously sync the cached routine, reset the edit draft, and bump the hydration ref the moment the restore POST returns — preventing the phantom "Unsaved routine edits" banner. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
fb2f836a1c
commit
d87f22a217
3 files changed
+83
-1
No files matched your search
@@ -317,4 +317,57 @@ describe("RoutineHistoryTab", () => {
|
||||
);
|
||||
expect(successCall).toBeTruthy();
|
||||
});
|
||||
|
||||
it("invokes onRestored with the restore response so the editor can rehydrate (PAP-3588)", async () => {
|
||||
const current = createRevision({ id: "revision-2", revisionNumber: 2 });
|
||||
const old = createRevision({
|
||||
id: "revision-1",
|
||||
revisionNumber: 1,
|
||||
snapshot: snapshotV1({ description: "Original description" }),
|
||||
});
|
||||
mockRoutinesApi.listRevisions.mockResolvedValue([current, old]);
|
||||
const restoredRoutine = createRoutine({
|
||||
description: "Original description",
|
||||
latestRevisionId: "revision-3",
|
||||
latestRevisionNumber: 3,
|
||||
});
|
||||
mockRoutinesApi.restoreRevision.mockResolvedValue({
|
||||
routine: restoredRoutine,
|
||||
revision: createRevision({
|
||||
id: "revision-3",
|
||||
revisionNumber: 3,
|
||||
restoredFromRevisionId: "revision-1",
|
||||
}),
|
||||
restoredFromRevisionId: "revision-1",
|
||||
restoredFromRevisionNumber: 1,
|
||||
secretMaterials: [],
|
||||
});
|
||||
const onRestored = vi.fn();
|
||||
await render({ onRestored });
|
||||
const oldRow = container.querySelector(
|
||||
"[data-testid='revision-row-1']",
|
||||
) as HTMLButtonElement | null;
|
||||
await act(async () => {
|
||||
oldRow?.click();
|
||||
});
|
||||
await flush();
|
||||
const restoreButtons = Array.from(container.querySelectorAll("button")).filter(
|
||||
(button) => button.textContent === "Restore as new revision",
|
||||
);
|
||||
await act(async () => {
|
||||
restoreButtons[0].click();
|
||||
});
|
||||
await flush();
|
||||
const confirmButtons = Array.from(container.querySelectorAll("button")).filter((b) =>
|
||||
(b.textContent ?? "").includes("Restore as revision 3"),
|
||||
);
|
||||
await act(async () => {
|
||||
confirmButtons[0].click();
|
||||
});
|
||||
await flush();
|
||||
expect(onRestored).toHaveBeenCalledTimes(1);
|
||||
const [response] = onRestored.mock.calls[0];
|
||||
expect(response.routine).toEqual(restoredRoutine);
|
||||
expect(response.revision.id).toBe("revision-3");
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,7 @@ type Props = {
|
||||
agents: AgentLookup;
|
||||
projects: ProjectLookup;
|
||||
onRestoreSecretMaterials: (response: RestoreRoutineRevisionResponse) => void;
|
||||
onRestored?: (response: RestoreRoutineRevisionResponse) => void;
|
||||
};
|
||||
|
||||
export function RoutineHistoryTab({
|
||||
@@ -59,6 +60,7 @@ export function RoutineHistoryTab({
|
||||
agents,
|
||||
projects,
|
||||
onRestoreSecretMaterials,
|
||||
onRestored,
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToastActions();
|
||||
@@ -116,6 +118,7 @@ export function RoutineHistoryTab({
|
||||
tone: "success",
|
||||
});
|
||||
onRestoreSecretMaterials(data);
|
||||
onRestored?.(data);
|
||||
setConfirmOpen(false);
|
||||
setRestoreSummary("");
|
||||
setSelectedRevisionId(data.revision.id);
|
||||
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { RoutineTrigger, RoutineVariable } from "@paperclipai/shared";
|
||||
import type { RoutineDetail as RoutineDetailType, RoutineTrigger, RoutineVariable } from "@paperclipai/shared";
|
||||
|
||||
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
||||
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
||||
@@ -1236,6 +1236,32 @@ export function RoutineDetail() {
|
||||
});
|
||||
}
|
||||
}}
|
||||
onRestored={(response: RestoreRoutineRevisionResponse) => {
|
||||
setSaveConflict(false);
|
||||
queryClient.setQueryData<RoutineDetailType | undefined>(
|
||||
queryKeys.routines.detail(routineId!),
|
||||
(prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
...response.routine,
|
||||
latestRevisionId: response.revision.id,
|
||||
latestRevisionNumber: response.revision.revisionNumber,
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
setEditDraft({
|
||||
title: response.routine.title,
|
||||
description: response.routine.description ?? "",
|
||||
projectId: response.routine.projectId ?? "",
|
||||
assigneeAgentId: response.routine.assigneeAgentId ?? "",
|
||||
priority: response.routine.priority,
|
||||
concurrencyPolicy: response.routine.concurrencyPolicy,
|
||||
catchUpPolicy: response.routine.catchUpPolicy,
|
||||
variables: response.routine.variables,
|
||||
});
|
||||
hydratedRoutineIdRef.current = response.routine.id;
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in new issue
Block a user