fix(artifacts): heal Starred tab + scope document-star rollback (Greptile)

- onSettled now invalidates the ["artifacts"] list queries too, so an
  unstarred document leaves the Starred tab immediately instead of lingering
  until the next natural refetch (the tab is a separate list query from the
  memberships cache).
- Rollback on error re-applies only the failed document's prior star on top of
  the current cache, instead of restoring a whole snapshot that could clobber a
  concurrent star mutation's optimistic state.
- Add hook test asserting the artifacts list is invalidated on settle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-07-21 15:05:33 +00:00
1 parent 19dfe83e56
commit a9752fc870
2 files changed
+40 -4

No files matched your search

@@ -149,6 +149,32 @@ describe("useDocumentStarMutation", () => {
expect(starredIds(queryClient)).toContain("doc-1");
});
it("invalidates the Artifacts list on settle so an unstarred doc leaves the Starred tab", async () => {
membershipsApiMock.listMine.mockResolvedValue({
...emptyMemberships(),
starredDocumentIds: ["doc-1"],
documentStarredAt: { "doc-1": new Date().toISOString() },
});
membershipsApiMock.updateDocument.mockResolvedValue({
resourceType: "document",
resourceId: "doc-1",
state: "left",
starredAt: null,
updatedAt: new Date(),
});
let handle: HarnessHandle | null = null;
const queryClient = await render((h) => { handle = h; });
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
await act(async () => {
handle!.mutate({ documentId: "doc-1", documentName: "Launch Brief", starred: false });
await flush();
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["artifacts"] });
});
it("rolls back the optimistic unstar and toasts when the server rejects", async () => {
membershipsApiMock.listMine.mockResolvedValue({
...emptyMemberships(),
+14 -4
View File
@@ -259,16 +259,22 @@ export function useDocumentStarMutation(companyId: string | null | undefined) {
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey });
const previous = queryClient.getQueryData<ResourceMemberships>(queryKey);
// Capture only this document's prior star so a rollback reverts just this
// change and leaves any concurrent star mutation's optimistic state intact.
const previousStarred = isStarred(previous, "document", variables.documentId);
queryClient.setQueryData<ResourceMemberships>(
queryKey,
applyDocumentStarChange(previous, variables.documentId, variables.starred),
);
return { previous };
return { previousStarred };
},
onError: (error, variables, context) => {
if (context?.previous) {
queryClient.setQueryData(queryKey, context.previous);
}
// Targeted rollback: re-apply this document's prior star on top of the
// current cache, so we don't clobber a newer concurrent update.
queryClient.setQueryData<ResourceMemberships>(
queryKey,
(current) => applyDocumentStarChange(current, variables.documentId, context?.previousStarred ?? false),
);
pushToast({
title: `Couldn't ${variables.starred ? "star" : "unstar"} ${variables.documentName}.`,
body: error instanceof Error ? error.message : "Try again.",
@@ -284,6 +290,10 @@ export function useDocumentStarMutation(companyId: string | null | undefined) {
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey });
// The Artifacts "Starred" tab is a separate list query keyed by ["artifacts", ...],
// so refresh it too — otherwise an unstarred document lingers in the grid
// until the next natural refetch.
queryClient.invalidateQueries({ queryKey: ["artifacts"] });
},
});
}