Skip to content

Commit e6cb9d4

Browse files
committed
fix(search): keep results painted when Show more widens the page
1 parent d1e8473 commit e6cb9d4

7 files changed

Lines changed: 76 additions & 24 deletions

File tree

‎apps/sim/app/o/[organizationId]/search/search-results-view.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ describe('results-only search layout', () => {
145145
const filters = mocks.search.mock.calls.at(-1)![2]
146146
expect(filters.source).toBe('slack')
147147
expect(mocks.searchChange).toHaveBeenLastCalledWith({ scope, query: 'Orion', filters })
148-
expect(mocks.search).toHaveBeenLastCalledWith(scope, 'Orion', filters, 20)
148+
expect(mocks.search).toHaveBeenLastCalledWith(scope, 'Orion', filters, 20, {
149+
retainAcrossLimits: true,
150+
})
149151
})
150152
it('summarizes the selected document through Home without duplicating search state', async () => {
151153
await render('?q=Orion')

‎apps/sim/app/o/[organizationId]/search/search.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ async function editDraft(value: string) {
163163
function expectVisibleQuery(query: string) {
164164
expect(searchInput().value).toBe(query)
165165
expect(container.querySelector('a[data-source-link]')?.textContent).toBe(`${query} launch plan`)
166-
expect(mocks.search).toHaveBeenLastCalledWith(scope, query, {}, 20)
166+
expect(mocks.search).toHaveBeenLastCalledWith(scope, query, {}, 20, {
167+
retainAcrossLimits: true,
168+
})
167169
expect(document.activeElement).toBe(searchInput())
168170
}
169171

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ describe('result paging and the custom window', () => {
195195
await act(async () => more()!.click())
196196
/** The wider search is its own request; the first paint was never widened. */
197197
expect(mocks.search.mock.calls.at(-1)![3]).toBe(50)
198+
expect(mocks.search.mock.calls.at(-1)![4]).toEqual({ retainAcrossLimits: true })
198199
expect(more()).toBeUndefined()
199200
mocks.search.mockReturnValue(page(7))
200201
await render()

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ function SearchResults({
201201
topK ??
202202
(expanded
203203
? WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.expanded
204-
: WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.initial)
204+
: WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.initial),
205+
{ retainAcrossLimits: topK === undefined }
205206
)
206207
/** A full first page may collapse to few cards, yet more documents may still match. */
207208
const mayHaveMore =

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-transitions.test.tsx‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,30 @@ async function click(label: string) {
174174

175175
async function complete(
176176
index: number,
177-
{ title = 'Release plan', partial = false, empty = false } = {}
177+
{ title = 'Release plan', partial = false, empty = false, count = 1 } = {}
178178
) {
179179
await act(async () => {
180180
requests[index].resolve({
181181
data: {
182182
query: requests[index].body.query,
183183
results: empty
184184
? []
185-
: [
186-
{
187-
documentId: title,
185+
: Array.from({ length: count }, (_, n) => {
186+
const name = n === 0 ? title : `${title} ${n + 1}`
187+
return {
188+
documentId: name,
188189
knowledgeBaseId: 'index',
189190
knowledgeBaseName: 'Search index',
190-
documentName: title,
191-
sourceUrl: 'https://example.com/release',
191+
documentName: name,
192+
sourceUrl: `https://example.com/release/${n}`,
192193
connectorType: requests[index].body.filters?.source ?? 'slack',
193194
sourceModifiedAt: null,
194195
author: null,
195196
content: 'launch details',
196197
chunkIndex: 0,
197198
similarity: 0.9,
198-
},
199-
],
199+
}
200+
}),
200201
retrieval: {
201202
status: partial ? 'partial' : 'complete',
202203
timedOutLegs: partial ? ['vector'] : [],
@@ -285,6 +286,24 @@ describe('search refinement with the real query cache and URL state', () => {
285286
expect(container.textContent).not.toContain('Release plan')
286287
})
287288

289+
it('keeps the first page painted while Show more widens it, then while a filter narrows it', async () => {
290+
await render()
291+
await complete(0, { count: 20 })
292+
await click('Show more')
293+
expect(requests[1].body.topK).toBe(50)
294+
expect(container.textContent).toContain('Updating results…')
295+
expect(container.textContent).not.toContain('Searching…')
296+
expect(container.querySelectorAll('a[data-source-link]')).toHaveLength(20)
297+
await complete(1, { title: 'Wider plan', count: 50 })
298+
expect(container.querySelectorAll('a[data-source-link]')).toHaveLength(50)
299+
await click('Gmail')
300+
expect(requests[2].body).toMatchObject({ filters: { source: 'gmail' }, topK: 20 })
301+
expect(container.textContent).toContain('Updating results…')
302+
expect(container.textContent).not.toContain('Searching…')
303+
expect(container.querySelectorAll('a[data-source-link]')).toHaveLength(50)
304+
expect(container.querySelector('a[data-source-link]')?.textContent).toBe('Wider plan')
305+
})
306+
288307
it('replaces filter URL state while preserving unrelated parameters', async () => {
289308
await render({ params: '?q=launch&panel=details' })
290309
await complete(0)

‎apps/sim/hooks/queries/kb/knowledge.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,24 @@ describe('knowledge query placeholder scope', () => {
243243
expect(placeholder('workspace-1', 'different', 5, 'reader')).toBeUndefined()
244244
})
245245

246+
it('retains a page-owned search across result limits only for the same reader', () => {
247+
const query = captureQuery(() =>
248+
useWorkspaceKnowledgeSearch('workspace-1', 'release', { source: 'slack' }, 50, {
249+
retainAcrossLimits: true,
250+
})
251+
)
252+
const previous = { results: [{ documentId: 'private-document' }] }
253+
mocks.getQueryData.mockReturnValue(previous)
254+
const placeholder = (topK: number, userId: string) =>
255+
query.placeholderData?.(previous, {
256+
queryKey: knowledgeKeys.search('workspace-1', 'release', {}, topK, userId),
257+
state: { status: 'success', isInvalidated: false },
258+
})
259+
expect(placeholder(20, 'reader')).toBe(previous)
260+
expect(placeholder(50, 'reader')).toBe(previous)
261+
expect(placeholder(20, 'other')).toBeUndefined()
262+
})
263+
246264
it('partitions search cache entries by filter and reader', () => {
247265
const query = captureQuery(() =>
248266
useWorkspaceKnowledgeSearch('workspace-1', 'new query', { source: 'slack' })

‎apps/sim/hooks/queries/kb/knowledge.ts‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,13 +1205,24 @@ async function searchWorkspaceKnowledge(
12051205
return data.data
12061206
}
12071207

1208+
interface WorkspaceKnowledgeSearchOptions {
1209+
nativeQueries?: NativeSearchQuery[]
1210+
reuseFreshResult?: boolean
1211+
/**
1212+
* Keeps the previous result painted when only the result limit changes. Set it when the
1213+
* surface owns the limit (Show more widens the same search); leave it off when the limit is
1214+
* part of what was asked for, so a new limit is a new search that never shows the old one.
1215+
*/
1216+
retainAcrossLimits?: boolean
1217+
}
1218+
12081219
/** Searches the canonical index under the signed-in person's ACLs. */
12091220
export function useWorkspaceKnowledgeSearch(
12101221
owner: string | ResourceScope | undefined,
12111222
query: string,
12121223
filters?: WorkspaceSearchFilters,
12131224
topK = 20,
1214-
options?: { nativeQueries?: NativeSearchQuery[]; reuseFreshResult?: boolean }
1225+
options?: WorkspaceKnowledgeSearchOptions
12151226
) {
12161227
const { features } = useDeploymentShape()
12171228
const live = features.liveEnterpriseSearch === true
@@ -1258,17 +1269,15 @@ export function useWorkspaceKnowledgeSearch(
12581269
: 0
12591270
: WORKSPACE_KNOWLEDGE_SEARCH_STALE_TIME,
12601271
retry: false,
1261-
placeholderData: (previous, previousQuery) =>
1262-
!live &&
1263-
userId &&
1264-
previousQuery?.state.status === 'success' &&
1265-
previousQuery.queryKey[6] === topK &&
1266-
!previousQuery.state.isInvalidated &&
1267-
knowledgeKeys
1268-
.searchQuery(scopeKey, trimmed, userId)
1269-
.every((part, index) => previousQuery.queryKey[index] === part) &&
1270-
queryClient.getQueryData(previousQuery.queryKey) === previous
1271-
? previous
1272-
: undefined,
1272+
placeholderData: (previous, previousQuery) => {
1273+
if (live || !userId || previousQuery?.state.status !== 'success') return undefined
1274+
if (previousQuery.state.isInvalidated) return undefined
1275+
const prefix = knowledgeKeys.searchQuery(scopeKey, trimmed, userId)
1276+
if (!prefix.every((part, index) => previousQuery.queryKey[index] === part)) return undefined
1277+
/** `search()` appends filters, then the limit, after the reader/query prefix. */
1278+
const previousTopK = previousQuery.queryKey[prefix.length + 1]
1279+
if (!options?.retainAcrossLimits && previousTopK !== topK) return undefined
1280+
return queryClient.getQueryData(previousQuery.queryKey) === previous ? previous : undefined
1281+
},
12731282
})
12741283
}

0 commit comments

Comments
 (0)