fix: refetch favorites when toggled before the list loads, lint fixes

An optimistic favorite written over an unpopulated cache seeded the list
with only the toggled item, and cancelQueries killed the initial fetch
that would have corrected it, hiding existing favorites until reload. The
optimistic write now only applies over known data; otherwise onSettled
invalidates so the authoritative list is refetched.

Also unnest the version date-label ternary and drop an unused form watch
flagged by CI.
This commit is contained in:
Marco Beretta 2026-07-03 03:57:36 +02:00
parent 2a3e14c850
commit 05ac524fd9
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
4 changed files with 53 additions and 13 deletions

View file

@ -53,11 +53,12 @@ export default function VersionItem({
const date = getTimestampDate(version);
const hasUpdatedAt = version.updatedAt != null;
const hasCreatedAt = version.createdAt != null;
const relativeLabel = date
? formatDistanceToNow(date, { addSuffix: true })
: hasUpdatedAt || hasCreatedAt
? localize('com_ui_agent_version_unknown_date')
: localize('com_ui_agent_version_no_date');
const fallbackDateLabel = localize(
hasUpdatedAt || hasCreatedAt
? 'com_ui_agent_version_unknown_date'
: 'com_ui_agent_version_no_date',
);
const relativeLabel = date ? formatDistanceToNow(date, { addSuffix: true }) : fallbackDateLabel;
const absoluteLabel = date ? date.toLocaleString() : relativeLabel;
const toolsCount = countItems(version.tools);

View file

@ -71,7 +71,7 @@ export default function ActionsPanel({
},
});
const { reset, watch } = methods;
const { reset } = methods;
useEffect(() => {
if (action?.metadata?.auth) {

View file

@ -62,13 +62,20 @@ export const useGetToolFavoritesQuery = (
export const useAddToolFavoriteMutation = () => {
const queryClient = useQueryClient();
return useMutation((favorite: TToolFavorite) => dataService.addToolFavorite(favorite), {
/** Optimistic writes only apply over known server data. Before the list
* query has populated, seeding the cache from `[]` would make the toggled
* item look like the user's only favorite (and `cancelQueries` kills the
* initial fetch that would correct it) so skip the write and let
* `onSettled` refetch the authoritative list instead. */
onMutate: async (favorite) => {
await queryClient.cancelQueries([QueryKeys.toolFavorites]);
const previous = queryClient.getQueryData<TToolFavorite[]>([QueryKeys.toolFavorites]);
queryClient.setQueryData<TToolFavorite[]>([QueryKeys.toolFavorites], (current) => {
const list = current ?? [];
return list.some((f) => sameFavorite(f, favorite)) ? list : [...list, favorite];
});
if (previous !== undefined) {
queryClient.setQueryData<TToolFavorite[]>(
[QueryKeys.toolFavorites],
previous.some((f) => sameFavorite(f, favorite)) ? previous : [...previous, favorite],
);
}
return { previous };
},
onError: (_err, _favorite, context) => {
@ -76,6 +83,11 @@ export const useAddToolFavoriteMutation = () => {
queryClient.setQueryData([QueryKeys.toolFavorites], context.previous);
}
},
onSettled: (_data, _err, _favorite, context) => {
if (context?.previous === undefined) {
queryClient.invalidateQueries([QueryKeys.toolFavorites]);
}
},
});
};
@ -85,9 +97,12 @@ export const useRemoveToolFavoriteMutation = () => {
onMutate: async (favorite) => {
await queryClient.cancelQueries([QueryKeys.toolFavorites]);
const previous = queryClient.getQueryData<TToolFavorite[]>([QueryKeys.toolFavorites]);
queryClient.setQueryData<TToolFavorite[]>([QueryKeys.toolFavorites], (current) =>
(current ?? []).filter((f) => !sameFavorite(f, favorite)),
);
if (previous !== undefined) {
queryClient.setQueryData<TToolFavorite[]>(
[QueryKeys.toolFavorites],
previous.filter((f) => !sameFavorite(f, favorite)),
);
}
return { previous };
},
onError: (_err, _favorite, context) => {
@ -95,5 +110,10 @@ export const useRemoveToolFavoriteMutation = () => {
queryClient.setQueryData([QueryKeys.toolFavorites], context.previous);
}
},
onSettled: (_data, _err, _favorite, context) => {
if (context?.previous === undefined) {
queryClient.invalidateQueries([QueryKeys.toolFavorites]);
}
},
});
};

View file

@ -98,6 +98,25 @@ describe('useToolFavorites', () => {
expect(result.current.isFavorite({ kind: 'action', id: 'a1' })).toBe(false);
});
test('starring before the list loads refetches instead of hiding existing favorites', async () => {
mockGetToolFavorites
.mockImplementationOnce(() => new Promise<TToolFavorite[]>(() => undefined))
.mockResolvedValueOnce([...seeded, { itemType: 'skill', itemId: 's1' }]);
const { result } = renderHook(() => useToolFavorites(), { wrapper: createWrapper() });
await act(async () => {
await result.current.toggle({ kind: 'skill', id: 's1' });
});
await waitFor(() =>
expect(result.current.favoriteKeys).toEqual(
new Set(['tool:dalle', 'mcp:everything', 'skill:s1']),
),
);
expect(mockGetToolFavorites).toHaveBeenCalledTimes(2);
});
test('shows the item-worded cap toast and rolls back on rejection', async () => {
mockAddToolFavorite.mockRejectedValue({
response: { data: { code: 'MAX_FAVORITES_EXCEEDED', limit: 100 } },