Skip to content

Commit 00e9d7c

Browse files
committed
fix: align time entry workspace scope
1 parent f8a3955 commit 00e9d7c

4 files changed

Lines changed: 158 additions & 6 deletions

File tree

apps/backend/internal/bootstrap/cache_regression_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,25 @@ func TestArchivedClientDetailInvalidatesCachedClient(t *testing.T) {
7373
t.Fatalf("expected client id, got %#v", clientBody)
7474
}
7575

76+
warmClient := performAuthorizedJSONRequest(
77+
t,
78+
app,
79+
http.MethodGet,
80+
"/api/v9/workspaces/"+intToString(workspaceID)+"/clients/"+intToString(clientBody.ID),
81+
nil,
82+
authorization,
83+
)
84+
if warmClient.Code != http.StatusOK {
85+
t.Fatalf("expected warm client detail status 200, got %d body=%s", warmClient.Code, warmClient.Body.String())
86+
}
87+
var activeClient struct {
88+
Archived bool `json:"archived"`
89+
}
90+
mustDecodeJSON(t, warmClient.Body.Bytes(), &activeClient)
91+
if activeClient.Archived {
92+
t.Fatalf("expected pre-archive client detail to show archived=false, got body=%s", warmClient.Body.String())
93+
}
94+
7695
archiveClient := performAuthorizedJSONRequest(
7796
t,
7897
app,

apps/website/src/shared/query/offline-optimistic.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* @vitest-environment jsdom */
12
import { QueryClient } from "@tanstack/react-query";
23
import { beforeEach, describe, expect, it, vi } from "vitest";
34

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,121 @@
1-
import { describe, expect, it } from "vitest";
1+
/* @vitest-environment jsdom */
2+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3+
import { renderHook, waitFor } from "@testing-library/react";
4+
import { createElement, type ReactNode } from "react";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
26

3-
import { timeEntriesQueryKey } from "./web-shell-time-entries.ts";
7+
const mockGetTimeEntries = vi.fn();
8+
const mockUpdateWebSession = vi.fn();
9+
10+
vi.mock("../api/public/track/index.ts", () => ({
11+
deleteWorkspaceTimeEntries: vi.fn(),
12+
getCurrentTimeEntry: vi.fn(),
13+
getTimeEntries: (...args: unknown[]) => mockGetTimeEntries(...args),
14+
patchWorkspaceStopTimeEntryHandler: vi.fn(),
15+
postWorkspaceTimeEntries: vi.fn(),
16+
putWorkspaceTimeEntryHandler: vi.fn(),
17+
}));
18+
19+
vi.mock("../api/web/index.ts", () => ({
20+
listRecentWorkspaceTimeEntrySuggestions: vi.fn(),
21+
updateWebSession: (...args: unknown[]) => mockUpdateWebSession(...args),
22+
}));
23+
24+
vi.mock("../api/web-client.ts", () => ({
25+
unwrapWebApiResult: (p: Promise<unknown>) =>
26+
p.then((result) =>
27+
typeof result === "object" && result !== null && "data" in result
28+
? (result as { data: unknown }).data
29+
: result,
30+
),
31+
}));
32+
33+
const { sessionQueryKey, timeEntriesQueryKey, useTimeEntriesQuery } =
34+
await import("./web-shell.ts");
35+
36+
function createTestQueryClient(): QueryClient {
37+
return new QueryClient({
38+
defaultOptions: {
39+
queries: { retry: false },
40+
},
41+
});
42+
}
43+
44+
function createWrapper(queryClient: QueryClient) {
45+
return function Wrapper({ children }: { children: ReactNode }) {
46+
return createElement(QueryClientProvider, { client: queryClient }, children);
47+
};
48+
}
449

550
describe("timeEntriesQueryKey", () => {
51+
beforeEach(() => {
52+
vi.clearAllMocks();
53+
});
54+
655
it("scopes time entry lists by workspace", () => {
756
expect(timeEntriesQueryKey(1, "2026-05-01", "2026-05-07", false)).not.toEqual(
857
timeEntriesQueryKey(2, "2026-05-01", "2026-05-07", false),
958
);
1059
});
60+
61+
it("aligns the server session workspace before reading /me time entries", async () => {
62+
const queryClient = createTestQueryClient();
63+
queryClient.setQueryData(sessionQueryKey, {
64+
current_workspace_id: 1,
65+
});
66+
mockUpdateWebSession.mockResolvedValue({
67+
data: {
68+
current_workspace_id: 2,
69+
},
70+
});
71+
mockGetTimeEntries.mockResolvedValue({ data: [] });
72+
73+
renderHook(
74+
() =>
75+
useTimeEntriesQuery({
76+
endDate: "2026-05-07",
77+
startDate: "2026-05-01",
78+
workspaceId: 2,
79+
}),
80+
{
81+
wrapper: createWrapper(queryClient),
82+
},
83+
);
84+
85+
await waitFor(() => expect(mockGetTimeEntries).toHaveBeenCalledTimes(1));
86+
expect(mockUpdateWebSession).toHaveBeenCalledWith({
87+
body: {
88+
workspace_id: 2,
89+
},
90+
});
91+
expect(mockGetTimeEntries.mock.invocationCallOrder[0]).toBeGreaterThan(
92+
mockUpdateWebSession.mock.invocationCallOrder[0],
93+
);
94+
expect(queryClient.getQueryData(sessionQueryKey)).toEqual({
95+
current_workspace_id: 2,
96+
});
97+
});
98+
99+
it("does not update the server session when the workspace is already current", async () => {
100+
const queryClient = createTestQueryClient();
101+
queryClient.setQueryData(sessionQueryKey, {
102+
current_workspace_id: 2,
103+
});
104+
mockGetTimeEntries.mockResolvedValue({ data: [] });
105+
106+
renderHook(
107+
() =>
108+
useTimeEntriesQuery({
109+
endDate: "2026-05-07",
110+
startDate: "2026-05-01",
111+
workspaceId: 2,
112+
}),
113+
{
114+
wrapper: createWrapper(queryClient),
115+
},
116+
);
117+
118+
await waitFor(() => expect(mockGetTimeEntries).toHaveBeenCalledTimes(1));
119+
expect(mockUpdateWebSession).not.toHaveBeenCalled();
120+
});
11121
});

apps/website/src/shared/query/web-shell-time-entries.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
putWorkspaceTimeEntryHandler,
1313
} from "../api/public/track/index.ts";
1414
import { listRecentWorkspaceTimeEntrySuggestions } from "../api/web/index.ts";
15+
import { updateWebSession } from "../api/web/index.ts";
1516

16-
import { toTrackUtcString } from "./web-shell.ts";
17+
import { sessionQueryKey, toTrackUtcString } from "./web-shell.ts";
1718

1819
export const timeEntriesQueryKey = (
1920
workspaceId: number,
@@ -32,6 +33,23 @@ const currentTimeEntryQueryKey = ["current-time-entry"] as const;
3233
const recentTimeEntrySuggestionsQueryKey = (workspaceId: number) =>
3334
["time-entry-suggestions", workspaceId] as const;
3435

36+
async function ensureTimeEntriesWorkspaceScope(
37+
queryClient: ReturnType<typeof useQueryClient>,
38+
workspaceId: number,
39+
) {
40+
const session = queryClient.getQueryData<{ current_workspace_id?: number }>(sessionQueryKey);
41+
if (session?.current_workspace_id === workspaceId) return;
42+
43+
const updated = await unwrapWebApiResult(
44+
updateWebSession({
45+
body: {
46+
workspace_id: workspaceId,
47+
},
48+
}),
49+
);
50+
queryClient.setQueryData(sessionQueryKey, updated);
51+
}
52+
3553
async function invalidateProjectRollups(
3654
queryClient: ReturnType<typeof useQueryClient>,
3755
workspaceId: number,
@@ -54,10 +72,13 @@ export function useTimeEntriesQuery(options: {
5472
startDate: string;
5573
workspaceId: number;
5674
}) {
75+
const queryClient = useQueryClient();
76+
5777
return useQuery({
5878
enabled: options.enabled ?? true,
59-
queryFn: () =>
60-
unwrapWebApiResult(
79+
queryFn: async () => {
80+
await ensureTimeEntriesWorkspaceScope(queryClient, options.workspaceId);
81+
return unwrapWebApiResult(
6182
getTimeEntries({
6283
query: {
6384
end_date: options?.endDate,
@@ -66,7 +87,8 @@ export function useTimeEntriesQuery(options: {
6687
start_date: options?.startDate,
6788
},
6889
}),
69-
),
90+
);
91+
},
7092
queryKey: timeEntriesQueryKey(
7193
options.workspaceId,
7294
options.startDate,

0 commit comments

Comments
 (0)