Skip to content

Commit 2996814

Browse files
committed
Merge remote-tracking branch 'origin/staging' into integrate/v2-w5
2 parents ea5cf64 + 618cee5 commit 2996814

3 files changed

Lines changed: 126 additions & 17 deletions

File tree

apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export async function prefetchKnowledgeBases(
3636
workspaceId: string,
3737
userId: string | undefined
3838
): Promise<void> {
39+
if (!userId) return
40+
3941
await Promise.all([
4042
queryClient.prefetchQuery({
4143
queryKey: knowledgeKeys.list(workspaceId, 'active'),

apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const {
1212
mockListFoldersForWorkspace,
1313
mockListInternalKnowledgeBases,
1414
mockListPinnedItemsForUser,
15+
mockListWorkflowsForUser,
16+
mockListWorkspacesForViewer,
17+
mockGetUserProfile,
18+
mockGetWorkspacePermissions,
19+
mockListMothershipChats,
1520
mockListTables,
1621
mockListWorkspaceFileFolders,
1722
mockListWorkspaceFilesWithShares,
@@ -23,6 +28,11 @@ const {
2328
mockListFoldersForWorkspace: vi.fn(),
2429
mockListInternalKnowledgeBases: vi.fn(),
2530
mockListPinnedItemsForUser: vi.fn(),
31+
mockListWorkflowsForUser: vi.fn(),
32+
mockListWorkspacesForViewer: vi.fn(),
33+
mockGetUserProfile: vi.fn(),
34+
mockGetWorkspacePermissions: vi.fn(),
35+
mockListMothershipChats: vi.fn(),
2636
mockListTables: vi.fn(),
2737
mockListWorkspaceFileFolders: vi.fn(),
2838
mockListWorkspaceFilesWithShares: vi.fn(),
@@ -45,6 +55,19 @@ vi.mock('@/lib/pinned-items/queries', () => ({
4555
}))
4656
vi.mock('@/lib/workspaces/permissions/utils', () => ({
4757
getWorkspaceMemberProfiles: mockGetWorkspaceMemberProfiles,
58+
getWorkspacePermissionsForAuthorizedViewer: mockGetWorkspacePermissions,
59+
}))
60+
vi.mock('@/lib/workflows/queries', () => ({
61+
listWorkflowsForUser: mockListWorkflowsForUser,
62+
}))
63+
vi.mock('@/lib/workspaces/list', () => ({
64+
listWorkspacesForViewer: mockListWorkspacesForViewer,
65+
}))
66+
vi.mock('@/lib/users/queries', () => ({
67+
getUserProfile: mockGetUserProfile,
68+
}))
69+
vi.mock('@/lib/copilot/chat/list-mothership-chats', () => ({
70+
listMothershipChats: mockListMothershipChats,
4871
}))
4972
vi.mock('@/lib/table/service', () => ({
5073
listTables: mockListTables,
@@ -74,6 +97,7 @@ vi.mock('@sim/emcn', () => ({
7497

7598
import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
7699
import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
100+
import { prefetchWorkspaceSidebar } from '@/app/workspace/[workspaceId]/prefetch'
77101
import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
78102
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
79103
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
@@ -98,6 +122,16 @@ describe('workspace list prefetches', () => {
98122
mockListWorkspaceFilesWithShares.mockResolvedValue([])
99123
mockListWorkspaceFileFolders.mockResolvedValue([])
100124
mockListPinnedItemsForUser.mockResolvedValue([])
125+
mockListWorkflowsForUser.mockResolvedValue([])
126+
mockGetUserProfile.mockResolvedValue({ id: USER_ID, name: 'Ada', email: 'a@b.c' })
127+
mockGetWorkspacePermissions.mockResolvedValue({ users: [] })
128+
mockListMothershipChats.mockResolvedValue([])
129+
mockListWorkspacesForViewer.mockResolvedValue({
130+
workspaces: [],
131+
lastActiveWorkspaceId: null,
132+
pinnedWorkspaceIds: [],
133+
creationPolicy: null,
134+
})
101135
mockGetWorkspaceMemberProfiles.mockResolvedValue([])
102136
mockListTables.mockResolvedValue([])
103137
mockAuthenticate.mockResolvedValue({ kind: 'session', userId: USER_ID, sessionId: 'sess-1' })
@@ -366,6 +400,84 @@ describe('workspace list prefetches', () => {
366400
}
367401
})
368402

403+
describe('prefetchWorkspaceSidebar / seedWorkspaceList', () => {
404+
const HOST_CONTEXT = {
405+
workspace: { id: WORKSPACE_ID },
406+
viewer: { permission: 'admin' },
407+
} as never
408+
409+
const WORKSPACE_ROW = {
410+
id: WORKSPACE_ID,
411+
name: 'GTM',
412+
ownerId: USER_ID,
413+
organizationId: null,
414+
workspaceMode: 'personal',
415+
permissions: 'admin',
416+
}
417+
418+
const LIST_PAYLOAD = {
419+
workspaces: [WORKSPACE_ROW],
420+
lastActiveWorkspaceId: null,
421+
pinnedWorkspaceIds: [],
422+
creationPolicy: null,
423+
}
424+
425+
/**
426+
* The load-bearing contract: an empty list must leave the key UNSET so the client
427+
* fetch reaches `GET /api/workspaces`' default-workspace creation path. Seeding an
428+
* empty array instead would suppress it and strand a brand-new viewer.
429+
*/
430+
it('seeds nothing when the viewer has no workspaces', async () => {
431+
mockListWorkspacesForViewer.mockResolvedValue({ ...LIST_PAYLOAD, workspaces: [] })
432+
const client = makeClient()
433+
434+
await prefetchWorkspaceSidebar(client, WORKSPACE_ID, USER_ID, HOST_CONTEXT, null)
435+
436+
expect(client.getQueryData(workspaceKeys.list('active'))).toBeUndefined()
437+
})
438+
439+
it('seeds the workspace list when the viewer has one', async () => {
440+
mockListWorkspacesForViewer.mockResolvedValue(LIST_PAYLOAD)
441+
const client = makeClient()
442+
443+
await prefetchWorkspaceSidebar(client, WORKSPACE_ID, USER_ID, HOST_CONTEXT, null)
444+
445+
const cached = client.getQueryData(workspaceKeys.list('active')) as
446+
| { workspaces: Array<{ id: string }> }
447+
| undefined
448+
expect(cached).toBeDefined()
449+
expect(cached?.workspaces.map((w) => w.id)).toEqual([WORKSPACE_ID])
450+
})
451+
452+
/** A failed seed is an optimization loss, not a render failure. */
453+
it('does not throw when the workspace read rejects, and seeds nothing', async () => {
454+
mockListWorkspacesForViewer.mockRejectedValue(new Error('500'))
455+
const client = makeClient()
456+
457+
await expect(
458+
prefetchWorkspaceSidebar(client, WORKSPACE_ID, USER_ID, HOST_CONTEXT, null)
459+
).resolves.toBeUndefined()
460+
expect(client.getQueryData(workspaceKeys.list('active'))).toBeUndefined()
461+
})
462+
463+
/** Guards the mismatch check that keeps one workspace's data out of another's cache. */
464+
it('seeds nothing when the host context is for a different workspace', async () => {
465+
mockListWorkspacesForViewer.mockResolvedValue(LIST_PAYLOAD)
466+
const client = makeClient()
467+
468+
await prefetchWorkspaceSidebar(
469+
client,
470+
WORKSPACE_ID,
471+
USER_ID,
472+
{ workspace: { id: 'other-ws' }, viewer: { permission: 'admin' } } as never,
473+
null
474+
)
475+
476+
expect(client.getQueryCache().getAll()).toHaveLength(0)
477+
expect(mockListWorkspacesForViewer).not.toHaveBeenCalled()
478+
})
479+
})
480+
369481
describe('graceful failure', () => {
370482
it.each([
371483
[
@@ -379,9 +491,14 @@ describe('workspace list prefetches', () => {
379491
tableKeys.list(WORKSPACE_ID, 'active'),
380492
],
381493
[
494+
/**
495+
* Asserted against the folder key, not the file list: `prefetchFilesBrowser`
496+
* deliberately never seeds `workspaceFilesKeys` (the layout owns it), so an
497+
* assertion on that key would hold no matter what this function did.
498+
*/
382499
'prefetchFilesBrowser',
383500
(client: QueryClient) => prefetchFilesBrowser(client, WORKSPACE_ID, USER_ID),
384-
workspaceFilesKeys.list(WORKSPACE_ID, 'active'),
501+
workspaceFileFolderKeys.list(WORKSPACE_ID, 'active'),
385502
],
386503
] as const)(
387504
'%s does not throw when the fetcher rejects (page still renders, client refetches)',
@@ -393,6 +510,7 @@ describe('workspace list prefetches', () => {
393510
mockListInternalKnowledgeBases.mockRejectedValue(boom)
394511
mockListPinnedItemsForUser.mockRejectedValue(boom)
395512
mockGetWorkspaceMemberProfiles.mockRejectedValue(boom)
513+
mockListWorkspaceFileFolders.mockRejectedValue(boom)
396514
const client = makeClient()
397515

398516
await expect(prefetch(client)).resolves.toBeUndefined()

apps/sim/app/workspace/[workspaceId]/prefetch.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import type { QueryClient } from '@tanstack/react-query'
44
import { listWorkspacesContract, type WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
55
import { listMothershipChats } from '@/lib/copilot/chat/list-mothership-chats'
66
import { isChatEnabled } from '@/lib/core/config/env-flags'
7-
import { listFoldersForWorkspace } from '@/lib/folders/queries'
87
import { getUserProfile } from '@/lib/users/queries'
98
import { listWorkflowsForUser } from '@/lib/workflows/queries'
109
import { listWorkspaceFilesWithShares } from '@/lib/workspace-files/queries'
1110
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
1211
import { listWorkspacesForViewer } from '@/lib/workspaces/list'
1312
import { getWorkspacePermissionsForAuthorizedViewer } from '@/lib/workspaces/permissions/utils'
13+
import { prefetchResourceFolders } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-folders'
1414
import {
1515
MOTHERSHIP_CHAT_LIST_STALE_TIME,
1616
mapChat,
@@ -21,7 +21,6 @@ import {
2121
USER_PROFILE_STALE_TIME,
2222
userProfileKeys,
2323
} from '@/hooks/queries/user-profile'
24-
import { FOLDER_LIST_STALE_TIME, folderKeys, mapFolder } from '@/hooks/queries/utils/folder-keys'
2524
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
2625
import { mapWorkflow, WORKFLOW_LIST_STALE_TIME } from '@/hooks/queries/utils/workflow-list-query'
2726
import { normalizeWorkspacesResponse } from '@/hooks/queries/utils/workspace-list-query'
@@ -59,9 +58,8 @@ const logger = createLogger('WorkspacePrefetch')
5958
* Seeded rather than prefetched so the empty-list case can decline to create a
6059
* cache entry at all: the route's default-workspace creation path must run on
6160
* the client, and an entry — even an empty one — would suppress it. Expressing
62-
* that as an absent seed keeps a normal state out of the error channel, where
63-
* it previously cost a full second re-read (`retry: 1`) to re-derive an outcome
64-
* already known.
61+
* that as an absent seed also keeps a routine state out of the error channel,
62+
* where it read as a failure rather than as "nothing to seed".
6563
*/
6664
async function seedWorkspaceList(
6765
queryClient: QueryClient,
@@ -122,9 +120,7 @@ async function seedWorkspaceList(
122120
* to produce, without routing a normal state through the error channel. That
123121
* matters because only a settled query is dehydrated: an unawaited read would be
124122
* dropped from the payload entirely, so the switcher would waterfall on every
125-
* cold load rather than paint populated. Seeding also skips the `retry` default,
126-
* which previously ran the whole read a second time, a retry delay later, purely
127-
* to re-derive an outcome already known.
123+
* cold load rather than paint populated.
128124
*/
129125
export async function prefetchWorkspaceSidebar(
130126
queryClient: QueryClient,
@@ -156,14 +152,7 @@ export async function prefetchWorkspaceSidebar(
156152
}),
157153
]
158154
: []),
159-
queryClient.prefetchQuery({
160-
queryKey: folderKeys.list(workspaceId, 'active', 'workflow'),
161-
queryFn: async () => {
162-
const rows = await listFoldersForWorkspace(workspaceId, 'active', 'workflow')
163-
return rows.map(mapFolder)
164-
},
165-
staleTime: FOLDER_LIST_STALE_TIME,
166-
}),
155+
prefetchResourceFolders(queryClient, workspaceId, 'workflow', userId),
167156
/**
168157
* The sidebar reads the workspace's files for its search modal, on EVERY workspace route — so this
169158
* query is registered by sidebar chrome before any page renders. That ordering is why it has to be

0 commit comments

Comments
 (0)