diff --git a/src/actions/rooms.test.ts b/src/actions/rooms.test.ts index ab666f16..86eca361 100644 --- a/src/actions/rooms.test.ts +++ b/src/actions/rooms.test.ts @@ -236,6 +236,7 @@ beforeEach(() => { testState.auditEntries = []; vi.clearAllMocks(); testState.rconSend.mockResolvedValue(true); + testState.revalidate.mockImplementation(() => undefined); }); describe("legacy room actions through the Hotel mutation service", () => { @@ -449,6 +450,70 @@ describe("legacy room actions through the Hotel mutation service", () => { expect(revalidatePath).toHaveBeenCalledWith("/admin/rooms"); }); + it.each([ + [ + "item update", + () => { + testState.selectRows = [[itemRow()]]; + return updateRoomItem({ roomId: 7, itemId: 11, x: 4 }); + }, + "/admin/rooms/7/furni", + ], + [ + "item delete", + () => { + testState.selectRows = [[itemRow()]]; + return deleteRoomItem({ roomId: 7, itemId: 11 }); + }, + "/admin/rooms/7/furni", + ], + [ + "bulk item delete", + () => { + testState.selectRows = [[itemRow()]]; + return bulkDeleteRoomItems({ roomId: 7, itemIds: [11] }); + }, + "/admin/rooms/7/furni", + ], + [ + "room update", + () => { + testState.selectRows = [[roomRow()]]; + return updateRoom({ id: 7, name: "After" }); + }, + "/admin/rooms/7", + ], + [ + "room delete", + () => { + testState.selectRows = [[roomRow()]]; + return deleteRoom({ id: 7 }); + }, + "/admin/rooms", + ], + ])( + "reports committed %s with refresh-needed context when revalidation fails", + async (_name, invoke, path) => { + testState.revalidate.mockImplementation(() => { + throw new Error("cache unavailable"); + }); + + const result = await invoke(); + + expect(result).toMatchObject({ + ok: true, + data: { + committed: true, + refreshNeeded: true, + warning: expect.stringMatching(/saved.*refresh/i), + }, + }); + expect(testState.committedWrites).toHaveLength(1); + expect(testState.auditEntries).toHaveLength(1); + expect(revalidatePath).toHaveBeenCalledWith(path); + }, + ); + it("rejects invalid runtime actions before RCON delivery", async () => { const result = await roomRconAction({ roomId: 7, action: "explode" }); expectFailure(result, /invalid/i); diff --git a/src/actions/rooms.ts b/src/actions/rooms.ts index 3ae34587..455d10ac 100644 --- a/src/actions/rooms.ts +++ b/src/actions/rooms.ts @@ -58,17 +58,32 @@ function notifyBestEffort(payload: Parameters[0]): void { } } +function revalidateCommittedPath(path: string): ActionResult { + try { + revalidatePath(path); + return actionOk(); + } catch { + return actionOk({ + committed: true, + refreshNeeded: true, + warning: + "Changes were saved, but the page could not refresh automatically. Refresh the page before trying the action again.", + }); + } +} + export async function updateRoomItem( payload: Record, ): Promise { const staff = await requirePermission(PERMS.ROOMS_EDIT); try { await executeLegacyHotelMutation(staff, "room-item.update", payload); - revalidatePath(`/admin/rooms/${String(payload.roomId)}/furni`); - return actionOk(); } catch (error) { return actionFailure(error); } + return revalidateCommittedPath( + `/admin/rooms/${String(payload.roomId)}/furni`, + ); } export async function bulkDeleteRoomItems(payload: { @@ -78,11 +93,10 @@ export async function bulkDeleteRoomItems(payload: { const staff = await requirePermission(PERMS.ROOMS_EDIT); try { await executeLegacyHotelMutation(staff, "room-item.bulk-delete", payload); - revalidatePath(`/admin/rooms/${payload.roomId}/furni`); - return actionOk(); } catch (error) { return actionFailure(error); } + return revalidateCommittedPath(`/admin/rooms/${payload.roomId}/furni`); } export async function deleteRoomItem(payload: { @@ -92,11 +106,10 @@ export async function deleteRoomItem(payload: { const staff = await requirePermission(PERMS.ROOMS_EDIT); try { await executeLegacyHotelMutation(staff, "room-item.delete", payload); - revalidatePath(`/admin/rooms/${payload.roomId}/furni`); - return actionOk(); } catch (error) { return actionFailure(error); } + return revalidateCommittedPath(`/admin/rooms/${payload.roomId}/furni`); } export async function roomRconAction(payload: { @@ -124,25 +137,25 @@ export async function deleteRoom(payload: { id: number; }): Promise { const staff = await requirePermission(PERMS.ROOMS_DELETE); + let target = `#${payload.id}`; try { const snapshot = await executeLegacyHotelMutation( staff, "room.delete", payload, ); - notifyBestEffort({ - action: "room_delete", - actor: staff.username, - target: - typeof snapshot.before?.name === "string" - ? snapshot.before.name - : `#${payload.id}`, - }); - revalidatePath("/admin/rooms"); - return actionOk(); + if (typeof snapshot.before?.name === "string") { + target = snapshot.before.name; + } } catch (error) { return actionFailure(error); } + notifyBestEffort({ + action: "room_delete", + actor: staff.username, + target, + }); + return revalidateCommittedPath("/admin/rooms"); } export async function updateRoom(payload: { @@ -155,9 +168,8 @@ export async function updateRoom(payload: { const staff = await requirePermission(PERMS.ROOMS_EDIT); try { await executeLegacyHotelMutation(staff, "room.update", payload); - revalidatePath(`/admin/rooms/${payload.id}`); - return actionOk(); } catch (error) { return actionFailure(error); } + return revalidateCommittedPath(`/admin/rooms/${payload.id}`); }