Skip to content

Commit fe59f0d

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(selectors): scope connector caches by dependencies
1 parent 58e793c commit fe59f0d

2 files changed

Lines changed: 114 additions & 9 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, describe, expect, it, vi } from 'vitest'
7+
import type { ConnectorConfigField } from '@/connectors/types'
8+
import type { SelectorDefinition, SelectorKey } from '@/hooks/selectors/types'
9+
10+
const { getSelectorDefinitionMock, useSelectorOptionsMock } = vi.hoisted(() => ({
11+
getSelectorDefinitionMock: vi.fn(),
12+
useSelectorOptionsMock: vi.fn(() => ({
13+
data: [],
14+
isLoading: false,
15+
isFetching: false,
16+
isFetchingMore: false,
17+
hasMore: false,
18+
truncated: false,
19+
error: null,
20+
})),
21+
}))
22+
23+
vi.mock('@sim/emcn', () => ({ ChipCombobox: () => null }))
24+
vi.mock('@sim/emcn/icons', () => ({ Loader: () => null }))
25+
vi.mock('next/navigation', () => ({
26+
useParams: () => ({ workspaceId: 'workspace-1', id: 'knowledge-1' }),
27+
}))
28+
vi.mock('@/hooks/selectors/registry', () => ({
29+
getSelectorDefinition: getSelectorDefinitionMock,
30+
}))
31+
vi.mock('@/hooks/selectors/use-selector-query', () => ({
32+
useSelectorOptions: useSelectorOptionsMock,
33+
useSelectorOptionDetail: () => ({ data: null }),
34+
useSelectorOptionDetails: () => [],
35+
}))
36+
vi.mock('@/hooks/use-debounce', () => ({ useDebounce: (value: string) => value }))
37+
38+
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'
39+
40+
let root: Root | null = null
41+
42+
afterEach(() => {
43+
act(() => root?.unmount())
44+
root = null
45+
document.body.innerHTML = ''
46+
vi.clearAllMocks()
47+
})
48+
49+
describe('ConnectorSelectorField cache scope', () => {
50+
it('changes only when a server-resolved selector dependency changes', () => {
51+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
52+
const definition = {
53+
key: 'jira.projects' as SelectorKey,
54+
serverResolvedContextFields: ['domain'],
55+
getQueryKey: () => ['selectors', 'jira.projects'],
56+
fetchList: async () => [],
57+
} as SelectorDefinition
58+
getSelectorDefinitionMock.mockReturnValue(definition)
59+
60+
const domainField: ConnectorConfigField = {
61+
id: 'domain-field',
62+
title: 'Domain',
63+
type: 'short-input',
64+
canonicalParamId: 'domain',
65+
mode: 'basic',
66+
}
67+
const projectField = {
68+
id: 'project',
69+
title: 'Project',
70+
type: 'selector',
71+
selectorKey: definition.key,
72+
dependsOn: ['domain-field'],
73+
} satisfies ConnectorConfigField & { selectorKey: SelectorKey }
74+
const configFields = [domainField, projectField]
75+
const container = document.createElement('div')
76+
document.body.appendChild(container)
77+
root = createRoot(container)
78+
79+
const render = (domain: string, unrelated: string) => {
80+
act(() =>
81+
root?.render(
82+
<ConnectorSelectorField
83+
field={projectField}
84+
value=''
85+
onChange={vi.fn()}
86+
credentialId='credential-1'
87+
sourceConfig={{ 'domain-field': domain, unrelated }}
88+
configFields={configFields}
89+
canonicalModes={{ domain: 'basic' }}
90+
/>
91+
)
92+
)
93+
return useSelectorOptionsMock.mock.calls.at(-1)?.[1].context.selectorCacheScope
94+
}
95+
96+
const initial = render('{{JIRA_DOMAIN}}', 'first')
97+
const afterUnrelatedEdit = render('{{JIRA_DOMAIN}}', 'second')
98+
const afterDependencyEdit = render('{{OTHER_DOMAIN}}', 'second')
99+
100+
expect(initial).toEqual(expect.any(String))
101+
expect(afterUnrelatedEdit).toBe(initial)
102+
expect(afterDependencyEdit).not.toBe(initial)
103+
})
104+
})

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useMemo, useState } from 'react'
44
import { ChipCombobox, type ComboboxOption } from '@sim/emcn'
55
import { Loader } from '@sim/emcn/icons'
6-
import { generateShortId } from '@sim/utils/id'
76
import { useParams } from 'next/navigation'
87
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
98
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
@@ -13,6 +12,10 @@ import type {
1312
ConfigFieldValue,
1413
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
1514
import type { ConnectorConfigField } from '@/connectors/types'
15+
import {
16+
createSelectorCacheScopeRegistry,
17+
scopeServerResolvedSelectorContext,
18+
} from '@/hooks/selectors/context-resolution'
1619
import { getSelectorDefinition } from '@/hooks/selectors/registry'
1720
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
1821
import {
@@ -46,15 +49,12 @@ export function ConnectorSelectorField({
4649
const params = useParams<{ workspaceId: string; id: string }>()
4750
const isMulti = Boolean(field.multi)
4851
const [searchTerm, setSearchTerm] = useState('')
49-
const selectorCacheScope = useMemo(
50-
() => generateShortId(),
51-
[field.id, field.selectorKey, credentialId, sourceConfig]
52-
)
52+
const definition = getSelectorDefinition(field.selectorKey)
53+
const selectorCacheScopes = useMemo(() => createSelectorCacheScopeRegistry(), [])
5354

5455
const context = useMemo<SelectorContext>(() => {
5556
const ctx: SelectorContext = {
5657
workspaceId: params.workspaceId,
57-
selectorCacheScope,
5858
}
5959
if (credentialId) ctx.oauthCredential = credentialId
6060
if (field.mimeType) ctx.mimeType = field.mimeType
@@ -69,7 +69,7 @@ export function ConnectorSelectorField({
6969
}
7070
}
7171

72-
return ctx
72+
return scopeServerResolvedSelectorContext(definition, ctx, selectorCacheScopes)
7373
}, [
7474
credentialId,
7575
field.mimeType,
@@ -78,7 +78,8 @@ export function ConnectorSelectorField({
7878
configFields,
7979
canonicalModes,
8080
params.workspaceId,
81-
selectorCacheScope,
81+
definition,
82+
selectorCacheScopes,
8283
])
8384

8485
const depsResolved = useMemo(() => {
@@ -126,7 +127,7 @@ export function ConnectorSelectorField({
126127
* implementations resolve a record by id, where a partial keystroke is a guaranteed
127128
* failed upstream request rather than an empty result.
128129
*/
129-
const resolvesUnknownIds = Boolean(getSelectorDefinition(field.selectorKey).resolvesUnknownIds)
130+
const resolvesUnknownIds = Boolean(definition.resolvesUnknownIds)
130131
const debouncedSearch = useDebounce(searchTerm.trim(), SEARCH_DEBOUNCE_MS)
131132
const { data: searchedOption } = useSelectorOptionDetail(field.selectorKey, {
132133
context,

0 commit comments

Comments
 (0)