Skip to content

Commit ce57f3d

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(selectors): preserve server truncation in option lists
1 parent 044660b commit ce57f3d

4 files changed

Lines changed: 86 additions & 12 deletions

File tree

apps/sim/hooks/queries/selectors.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,70 @@ describe('generic selector queries', () => {
235235
}
236236
)
237237

238+
it.each([true, false, undefined])(
239+
'preserves flat selector truncation %s and clears it after a complete refetch',
240+
async (truncated) => {
241+
const items = [{ id: 'label-1', label: 'First label' }]
242+
mockExecuteSelectorRequest.mockResolvedValue({
243+
kind: 'list',
244+
items,
245+
...(truncated !== undefined ? { truncated } : {}),
246+
})
247+
const hook = renderHookWithClient(() =>
248+
useSelectorOptions('gmail.labels', {
249+
context: { workspaceId: 'workspace-1', oauthCredential: 'credential-1' },
250+
surfaceId: 'connector:gmail:label',
251+
})
252+
)
253+
254+
await waitFor(() => expect(hook.getResult().isSuccess).toBe(true))
255+
expect(hook.getResult()).toMatchObject({
256+
data: items,
257+
hasMore: false,
258+
truncated: truncated === true,
259+
})
260+
261+
const refreshedItems = [{ id: 'label-2', label: 'Complete results' }]
262+
mockExecuteSelectorRequest.mockResolvedValue({ kind: 'list', items: refreshedItems })
263+
act(() => hook.getResult().refetch())
264+
265+
await waitFor(() => expect(hook.getResult().data).toEqual(refreshedItems))
266+
expect(hook.getResult()).toMatchObject({ hasMore: false, truncated: false })
267+
}
268+
)
269+
270+
it('retains server truncation from earlier pages after loading the final page', async () => {
271+
mockExecuteSelectorRequest.mockImplementation(
272+
async ({ request }: { request: { cursor?: string } }) =>
273+
request.cursor
274+
? { kind: 'list', items: [{ id: 'workspace-2', label: 'Second' }], truncated: false }
275+
: {
276+
kind: 'list',
277+
items: [{ id: 'workspace-1', label: 'First' }],
278+
nextCursor: 'next-page',
279+
truncated: true,
280+
}
281+
)
282+
const hook = renderHookWithClient(() =>
283+
useSelectorOptions('bitbucket.workspaces', {
284+
context: { workspaceId: 'workspace-1', oauthCredential: 'credential-1' },
285+
surfaceId: 'canvas:block-1:workspace',
286+
})
287+
)
288+
289+
await waitFor(() => expect(hook.getResult().hasMore).toBe(true))
290+
expect(hook.getResult().truncated).toBe(true)
291+
292+
act(() => hook.getResult().loadMore())
293+
await waitFor(() =>
294+
expect(hook.getResult().data).toEqual([
295+
{ id: 'workspace-1', label: 'First' },
296+
{ id: 'workspace-2', label: 'Second' },
297+
])
298+
)
299+
expect(hook.getResult()).toMatchObject({ hasMore: false, truncated: true })
300+
})
301+
238302
it('loads paginated selectors on demand without putting cursors in the base key', async () => {
239303
mockExecuteSelectorRequest.mockImplementation(
240304
async ({ request }: { request: { cursor?: string } }) =>

apps/sim/hooks/queries/selectors.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import {
1212
} from '@/lib/selectors/manifest'
1313
import type {
1414
SelectorContext,
15+
SelectorExecutionResult,
1516
SelectorOption,
1617
SelectorPage,
1718
SelectorScope,
1819
} from '@/lib/selectors/types'
1920
import { selectorKeys } from '@/hooks/queries/utils/selector-keys'
2021

21-
const EMPTY_PAGE: SelectorPage = { items: [] }
22+
type SelectorListResult = Extract<SelectorExecutionResult, { kind: 'list' }>
23+
24+
const EMPTY_PAGE: SelectorListResult = { kind: 'list', items: [] }
2225
let nextOpaqueRevision = 1
2326

2427
export type SelectorClientContext = SelectorContext & {
@@ -167,7 +170,7 @@ export function useSelectorOptions(
167170
prepared.revision
168171
)
169172

170-
const flatQuery = useQuery<SelectorOption[]>({
173+
const flatQuery = useQuery<SelectorListResult>({
171174
// rq-lint-allow: context and search are represented by an opaque privacy revision.
172175
queryKey: baseKey,
173176
queryFn: async ({ signal }) => {
@@ -182,14 +185,14 @@ export function useSelectorOptions(
182185
signal,
183186
})
184187
if (result.kind !== 'list') throw new Error('Selector returned an unexpected detail result')
185-
return result.items
188+
return result
186189
},
187190
enabled: !supportsPagination && prepared.ready,
188191
staleTime: prepared.manifest.staleTime,
189192
gcTime: 0,
190193
})
191194

192-
const pagedQuery = useInfiniteQuery<SelectorPage>({
195+
const pagedQuery = useInfiniteQuery<SelectorListResult>({
193196
// rq-lint-allow: context and search are represented by an opaque privacy revision.
194197
queryKey: [...baseKey, 'paged'],
195198
queryFn: async ({ pageParam, signal }) => {
@@ -306,7 +309,10 @@ export function useSelectorOptions(
306309
isFetchingMore: pagedQuery.isFetchingNextPage || isLoadingAll,
307310
isLoadingAll,
308311
hasMore: canLoadMore,
309-
truncated: collectedOptions.overflowed || (Boolean(pagedQuery.hasNextPage) && reachedLoadCap),
312+
truncated:
313+
pagedQuery.data?.pages.some((page) => page.truncated === true) === true ||
314+
collectedOptions.overflowed ||
315+
(Boolean(pagedQuery.hasNextPage) && reachedLoadCap),
310316
error: (pagedQuery.error as Error | null) ?? null,
311317
isSuccess: pagedQuery.isSuccess,
312318
loadMore,
@@ -321,13 +327,13 @@ export function useSelectorOptions(
321327
}
322328
}
323329
return {
324-
data: flatQuery.data,
330+
data: flatQuery.data?.items,
325331
isLoading: flatQuery.isLoading,
326332
isFetching: flatQuery.isFetching,
327333
isFetchingMore: false,
328334
isLoadingAll: false,
329335
hasMore: false,
330-
truncated: false,
336+
truncated: flatQuery.data?.truncated === true,
331337
error: (flatQuery.error as Error | null) ?? null,
332338
isSuccess: flatQuery.isSuccess,
333339
loadMore: () => undefined,

packages/emcn/src/components/combobox/combobox.dom.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,16 @@ describe('Combobox pagination', () => {
262262
expect(document.body.textContent).not.toContain('Search all options')
263263
})
264264

265-
it('explains when provider results remain beyond the safety limit', () => {
266-
render(<Combobox options={OPTIONS} truncated />)
265+
it('explains truncated results without assuming a provider limit', () => {
266+
render(<Combobox options={OPTIONS} truncated searchable />)
267267

268268
click(trigger())
269269

270-
expect(document.body.textContent).toContain('Showing the first 10,000 options')
270+
expect(document.body.textContent).toContain('Showing partial results')
271+
272+
type(trigger('input[placeholder="Search..."]') as HTMLInputElement, 'missing')
273+
274+
expect(document.body.textContent).toContain('No matches in partial results')
271275
})
272276
})
273277

packages/emcn/src/components/combobox/combobox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ const Combobox = memo(
691691
const isLoadingContinuation = isLoadingMore || isLoadingAll
692692
const resolvedEmptyMessage =
693693
truncated && hasActiveSearch
694-
? 'No matches in the first 10,000 options'
694+
? 'No matches in partial results'
695695
: hasMore && hasActiveSearch
696696
? 'No matches in loaded options'
697697
: hasMore
@@ -728,7 +728,7 @@ const Combobox = memo(
728728
</Button>
729729
) : truncated && filteredOptions.length > 0 ? (
730730
<div className='py-2 text-center text-[var(--text-muted)] text-caption'>
731-
Showing the first 10,000 options
731+
Showing partial results
732732
</div>
733733
) : null
734734

0 commit comments

Comments
 (0)