Skip to content

Commit 9c102f2

Browse files
committed
improvement(resources): prefetch pinned ids and members with the resource lists
Files, Tables, and Knowledge already prefetched their items and folders, but not the two lists a complete row needs. Pinned ids are the list's primary sort key now, so a page that painted before they arrived rendered the whole list in the wrong order and then visibly re-sorted. Members back the Owner column, which painted empty and filled in after. Both now hydrate alongside the lists via `prefetchResourceListChrome`. Pinned keys move to `hooks/queries/utils/pinned-item-keys` so the server prefetch can address them without pulling the contracts barrel and the optimistic-mutation machinery into the route.
1 parent 8dc7789 commit 9c102f2

7 files changed

Lines changed: 137 additions & 14 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query'
22
import type { WorkspaceFileFolderApi } from '@/lib/api/contracts/workspace-file-folders'
33
import type { ListWorkspaceFilesResponse } from '@/lib/api/contracts/workspace-files'
44
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
5+
import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome'
56
import {
67
WORKSPACE_FILE_FOLDERS_STALE_TIME,
78
workspaceFileFolderKeys,
@@ -12,7 +13,9 @@ import {
1213
} from '@/hooks/queries/workspace-files'
1314

1415
/**
15-
* Prefetches the Files browser's two lists — workspace files and file folders —
16+
* Prefetches everything the Files browser needs to paint a complete, correctly-ordered
17+
* first frame: workspace files, file folders, and (via {@link prefetchResourceListChrome})
18+
* the pinned ids that drive row order plus the members behind the Owner column —
1619
* under the same query keys their client hooks (`useWorkspaceFiles`,
1720
* `useWorkspaceFileFolders`) use (scope `active`), so the browser paints
1821
* populated on first render.
@@ -45,5 +48,6 @@ export async function prefetchFilesBrowser(
4548
},
4649
staleTime: WORKSPACE_FILE_FOLDERS_STALE_TIME,
4750
}),
51+
prefetchResourceListChrome(queryClient, workspaceId, 'file'),
4852
])
4953
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { QueryClient } from '@tanstack/react-query'
22
import type { FolderApi } from '@/lib/api/contracts/folders'
33
import type { KnowledgeBaseData } from '@/lib/api/contracts/knowledge'
44
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
5+
import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome'
56
import { FOLDER_LIST_STALE_TIME, folderKeys, mapFolder } from '@/hooks/queries/utils/folder-keys'
67
import { KNOWLEDGE_BASE_LIST_STALE_TIME, knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
78

89
/**
9-
* Prefetches the workspace's knowledge-bases list AND its knowledge-base folder tree under
10+
* Prefetches the workspace's knowledge-bases list AND its knowledge-base folder tree — plus
11+
* the pinned ids and members {@link prefetchResourceListChrome} covers — under
1012
* the same query keys the client `useKnowledgeBasesQuery` / `useFolders` hooks use (scope
1113
* `active`), so the list paints populated on first render.
1214
*
@@ -43,5 +45,6 @@ export async function prefetchKnowledgeBases(
4345
},
4446
staleTime: FOLDER_LIST_STALE_TIME,
4547
}),
48+
prefetchResourceListChrome(queryClient, workspaceId, 'knowledge_base'),
4649
])
4750
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { QueryClient } from '@tanstack/react-query'
2+
import type { PinnedItemApi, PinnedResourceType } from '@/lib/api/contracts/pinned-items'
3+
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
4+
import { PINNED_ITEMS_STALE_TIME, pinnedItemKeys } from '@/hooks/queries/utils/pinned-item-keys'
5+
import {
6+
WORKSPACE_MEMBERS_STALE_TIME,
7+
type WorkspaceMember,
8+
workspaceKeys,
9+
} from '@/hooks/queries/workspace'
10+
11+
/**
12+
* Prefetches the two lists every foldered resource page needs to paint a row completely,
13+
* beyond the resources themselves.
14+
*
15+
* Pinned ids are not decoration: they are the list's primary sort key, so a page that paints
16+
* before they land renders the whole list in the wrong order and then visibly re-sorts. Two
17+
* lists are needed because a folder pins under `resourceType: 'folder'`, a different pin
18+
* namespace from the resource beside it.
19+
*
20+
* Members back the Owner column; without them every owner cell paints empty and fills in
21+
* after. Both are cheap and shared with the page's own list prefetch in one `Promise.all`.
22+
*/
23+
export async function prefetchResourceListChrome(
24+
queryClient: QueryClient,
25+
workspaceId: string,
26+
resourceType: PinnedResourceType
27+
): Promise<void> {
28+
const prefetchPinned = (type: PinnedResourceType) =>
29+
queryClient.prefetchQuery({
30+
queryKey: pinnedItemKeys.list(workspaceId, type),
31+
queryFn: async () => {
32+
const { pinnedItems } = await prefetchInternalJson<{ pinnedItems: PinnedItemApi[] }>(
33+
`/api/pinned-items?workspaceId=${workspaceId}&resourceType=${type}`
34+
)
35+
return pinnedItems
36+
},
37+
staleTime: PINNED_ITEMS_STALE_TIME,
38+
})
39+
40+
await Promise.all([
41+
prefetchPinned(resourceType),
42+
prefetchPinned('folder'),
43+
queryClient.prefetchQuery({
44+
queryKey: workspaceKeys.members(workspaceId),
45+
queryFn: async () => {
46+
const { members } = await prefetchInternalJson<{ members: WorkspaceMember[] }>(
47+
`/api/workspaces/${workspaceId}/members`
48+
)
49+
return members
50+
},
51+
staleTime: WORKSPACE_MEMBERS_STALE_TIME,
52+
}),
53+
])
54+
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/
2222
import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
2323
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
2424
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
25+
import { pinnedItemKeys } from '@/hooks/queries/utils/pinned-item-keys'
2526
import { tableKeys } from '@/hooks/queries/utils/table-keys'
27+
import { workspaceKeys } from '@/hooks/queries/workspace'
2628
import { workspaceFileFolderKeys } from '@/hooks/queries/workspace-file-folders'
2729
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
2830

@@ -102,6 +104,52 @@ describe('workspace list prefetches', () => {
102104
})
103105
})
104106

107+
describe('resource-list chrome', () => {
108+
/**
109+
* Pinned ids are the list's primary sort key, so a page that paints without them renders
110+
* the whole list in the wrong order and then visibly re-sorts. Members back the Owner
111+
* column. Both must be primed on every foldered page, under the exact client keys.
112+
*/
113+
const chromeCases = [
114+
{ name: 'files', run: prefetchFilesBrowser, resourceType: 'file' as const },
115+
{ name: 'tables', run: prefetchTables, resourceType: 'table' as const },
116+
{ name: 'knowledge', run: prefetchKnowledgeBases, resourceType: 'knowledge_base' as const },
117+
]
118+
119+
for (const { name, run, resourceType } of chromeCases) {
120+
it(`primes pinned ids (${resourceType} + folder) and members for ${name}`, async () => {
121+
const pinnedItems = [{ id: 'p-1', resourceId: 'r-1' }]
122+
const members = [{ userId: 'u-1', name: 'Ada' }]
123+
mockPrefetchInternalJson.mockImplementation(async (path: string) => {
124+
if (path.startsWith('/api/pinned-items')) return { pinnedItems }
125+
if (path.endsWith('/members')) return { members }
126+
if (path.includes('/folders')) return { folders: [] }
127+
return { success: true, files: [], data: { tables: [] } }
128+
})
129+
const client = makeClient()
130+
131+
await run(client, WORKSPACE_ID)
132+
133+
expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
134+
`/api/pinned-items?workspaceId=${WORKSPACE_ID}&resourceType=${resourceType}`
135+
)
136+
expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
137+
`/api/pinned-items?workspaceId=${WORKSPACE_ID}&resourceType=folder`
138+
)
139+
expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
140+
`/api/workspaces/${WORKSPACE_ID}/members`
141+
)
142+
expect(client.getQueryData(pinnedItemKeys.list(WORKSPACE_ID, resourceType))).toEqual(
143+
pinnedItems
144+
)
145+
expect(client.getQueryData(pinnedItemKeys.list(WORKSPACE_ID, 'folder'))).toEqual(
146+
pinnedItems
147+
)
148+
expect(client.getQueryData(workspaceKeys.members(WORKSPACE_ID))).toEqual(members)
149+
})
150+
}
151+
})
152+
105153
describe('prefetchHomeLists', () => {
106154
it('primes folder + file keys, mapping folder rows to the client shape', async () => {
107155
const folderRow = {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { QueryClient } from '@tanstack/react-query'
22
import type { FolderApi } from '@/lib/api/contracts/folders'
33
import type { TableDefinition } from '@/lib/table'
44
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
5+
import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome'
56
import { FOLDER_LIST_STALE_TIME, folderKeys, mapFolder } from '@/hooks/queries/utils/folder-keys'
67
import { TABLE_LIST_STALE_TIME, tableKeys } from '@/hooks/queries/utils/table-keys'
78

89
/**
9-
* Prefetches the workspace's tables list and its table folder tree under the same
10+
* Prefetches the workspace's tables list and its table folder tree — plus the pinned ids and
11+
* members {@link prefetchResourceListChrome} covers — under the same
1012
* query keys the client `useTablesList` / `useFolders` hooks use (scope `active`),
1113
* so the list paints populated on first render. Both are needed: a table row is
1214
* only placed correctly relative to the folder rows it sits beside, so
@@ -39,5 +41,6 @@ export async function prefetchTables(queryClient: QueryClient, workspaceId: stri
3941
},
4042
staleTime: FOLDER_LIST_STALE_TIME,
4143
}),
44+
prefetchResourceListChrome(queryClient, workspaceId, 'table'),
4245
])
4346
}

apps/sim/hooks/queries/pinned-items.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,7 @@ import {
1515
type PinnedItemApi,
1616
type PinnedResourceType,
1717
} from '@/lib/api/contracts'
18-
19-
export const PINNED_ITEMS_STALE_TIME = 60 * 1000
20-
21-
export const pinnedItemKeys = {
22-
all: ['pinnedItems'] as const,
23-
lists: () => [...pinnedItemKeys.all, 'list'] as const,
24-
/** Prefix covering every per-resourceType list in a workspace — the invalidation target. */
25-
workspaceLists: (workspaceId?: string) => [...pinnedItemKeys.lists(), workspaceId ?? ''] as const,
26-
list: (workspaceId?: string, resourceType?: PinnedResourceType) =>
27-
[...pinnedItemKeys.workspaceLists(workspaceId), resourceType ?? ''] as const,
28-
}
18+
import { PINNED_ITEMS_STALE_TIME, pinnedItemKeys } from '@/hooks/queries/utils/pinned-item-keys'
2919

3020
async function fetchPinnedItems(
3121
workspaceId: string,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { PinnedResourceType } from '@/lib/api/contracts/pinned-items'
2+
3+
/**
4+
* Lives in this standalone module — like {@link file://./folder-keys.ts} and
5+
* {@link file://./table-keys.ts} — so a server prefetch can hydrate the pinned lists without
6+
* importing `@/hooks/queries/pinned-items`, which pulls the contracts barrel and the
7+
* optimistic-mutation machinery in with it. The contract import here is type-only, so it
8+
* erases at build time.
9+
*/
10+
11+
/** Shared with the server prefetch so a hydrated list and a client fetch never disagree. */
12+
export const PINNED_ITEMS_STALE_TIME = 60 * 1000
13+
14+
export const pinnedItemKeys = {
15+
all: ['pinnedItems'] as const,
16+
lists: () => [...pinnedItemKeys.all, 'list'] as const,
17+
/** Prefix covering every per-resourceType list in a workspace — the invalidation target. */
18+
workspaceLists: (workspaceId?: string) => [...pinnedItemKeys.lists(), workspaceId ?? ''] as const,
19+
list: (workspaceId?: string, resourceType?: PinnedResourceType) =>
20+
[...pinnedItemKeys.workspaceLists(workspaceId), resourceType ?? ''] as const,
21+
}

0 commit comments

Comments
 (0)