Skip to content

Commit 94202ed

Browse files
authored
improvement(knowledge): show selector with saved option in connector edit modal (#4240)
* improvement(knowledge): show selector with saved option in connector edit modal * fix(kb-connectors): clear canonical siblings when non-canonical dep changes; share selector field * refactor(kb-connectors): extract canonical-field logic into useConnectorConfigFields hook * fix(kb-connectors): only merge changed fields into sourceConfig on edit save Avoids writing spurious empty-string keys for untouched optional fields when another field triggers a save. * refactor(kb-connectors): tighten state primitives in modals - edit modal: replace useMemo([]) + eslint-disable with useState lazy initializer for initialSourceConfig — same mount-once semantics without the escape hatch. - add modal: drop useCallback on handleConnectNewAccount (no observer saw the reference) and inline the one call site.
1 parent 802f4cf commit 94202ed

File tree

5 files changed

+306
-169
lines changed

5 files changed

+306
-169
lines changed

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx

Lines changed: 19 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useMemo, useState } from 'react'
3+
import { useMemo, useState } from 'react'
44
import { ArrowLeft, ArrowLeftRight, Loader2, Plus, Search } from 'lucide-react'
55
import { useParams } from 'next/navigation'
66
import {
@@ -23,11 +23,11 @@ import { getSubscriptionAccessState } from '@/lib/billing/client'
2323
import { consumeOAuthReturnContext } from '@/lib/credentials/client-state'
2424
import { getProviderIdFromServiceId, type OAuthProvider } from '@/lib/oauth'
2525
import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal'
26-
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/components/connector-selector-field'
26+
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field'
2727
import { SYNC_INTERVALS } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/consts'
2828
import { MaxBadge } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/max-badge'
29+
import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
2930
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
30-
import { getDependsOnFields } from '@/blocks/utils'
3131
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
3232
import type { ConnectorConfig, ConnectorConfigField } from '@/connectors/types'
3333
import { useCreateConnector } from '@/hooks/queries/kb/connectors'
@@ -57,13 +57,11 @@ export function AddConnectorModal({
5757
}: AddConnectorModalProps) {
5858
const [step, setStep] = useState<Step>(() => (initialConnectorType ? 'configure' : 'select-type'))
5959
const [selectedType, setSelectedType] = useState<string | null>(initialConnectorType ?? null)
60-
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
6160
const [syncInterval, setSyncInterval] = useState(1440)
6261
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
6362
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(() => new Set())
6463
const [error, setError] = useState<string | null>(null)
6564
const [showOAuthModal, setShowOAuthModal] = useState(false)
66-
const [canonicalModes, setCanonicalModes] = useState<Record<string, 'basic' | 'advanced'>>({})
6765

6866
const [apiKeyValue, setApiKeyValue] = useState('')
6967
const [apiKeyFocused, setApiKeyFocused] = useState(false)
@@ -100,54 +98,17 @@ export function AddConnectorModal({
10098
const effectiveCredentialId =
10199
selectedCredentialId ?? (credentials.length === 1 ? credentials[0].id : null)
102100

103-
const canonicalGroups = useMemo(() => {
104-
if (!connectorConfig) return new Map<string, ConnectorConfigField[]>()
105-
const groups = new Map<string, ConnectorConfigField[]>()
106-
for (const field of connectorConfig.configFields) {
107-
if (field.canonicalParamId) {
108-
const existing = groups.get(field.canonicalParamId)
109-
if (existing) {
110-
existing.push(field)
111-
} else {
112-
groups.set(field.canonicalParamId, [field])
113-
}
114-
}
115-
}
116-
return groups
117-
}, [connectorConfig])
118-
119-
const dependentFieldIds = useMemo(() => {
120-
if (!connectorConfig) return new Map<string, string[]>()
121-
const map = new Map<string, string[]>()
122-
for (const field of connectorConfig.configFields) {
123-
const deps = getDependsOnFields(field.dependsOn)
124-
for (const dep of deps) {
125-
const existing = map.get(dep) ?? []
126-
existing.push(field.id)
127-
map.set(dep, existing)
128-
}
129-
}
130-
for (const group of canonicalGroups.values()) {
131-
const allDependents = new Set<string>()
132-
for (const field of group) {
133-
for (const dep of map.get(field.id) ?? []) {
134-
allDependents.add(dep)
135-
const depField = connectorConfig.configFields.find((f) => f.id === dep)
136-
if (depField?.canonicalParamId) {
137-
for (const sibling of canonicalGroups.get(depField.canonicalParamId) ?? []) {
138-
allDependents.add(sibling.id)
139-
}
140-
}
141-
}
142-
}
143-
if (allDependents.size > 0) {
144-
for (const field of group) {
145-
map.set(field.id, [...allDependents])
146-
}
147-
}
148-
}
149-
return map
150-
}, [connectorConfig, canonicalGroups])
101+
const {
102+
sourceConfig,
103+
setSourceConfig,
104+
canonicalModes,
105+
setCanonicalModes,
106+
canonicalGroups,
107+
isFieldVisible,
108+
handleFieldChange,
109+
toggleCanonicalMode,
110+
resolveSourceConfig,
111+
} = useConnectorConfigFields({ connectorConfig })
151112

152113
const handleSelectType = (type: string) => {
153114
setSelectedType(type)
@@ -163,64 +124,6 @@ export function AddConnectorModal({
163124
onConnectorTypeChange?.(type)
164125
}
165126

166-
const handleFieldChange = useCallback(
167-
(fieldId: string, value: string) => {
168-
setSourceConfig((prev) => {
169-
const next = { ...prev, [fieldId]: value }
170-
const toClear = dependentFieldIds.get(fieldId)
171-
if (toClear) {
172-
for (const depId of toClear) {
173-
next[depId] = ''
174-
}
175-
}
176-
return next
177-
})
178-
},
179-
[dependentFieldIds]
180-
)
181-
182-
const toggleCanonicalMode = useCallback((canonicalId: string) => {
183-
setCanonicalModes((prev) => ({
184-
...prev,
185-
[canonicalId]: prev[canonicalId] === 'advanced' ? 'basic' : 'advanced',
186-
}))
187-
}, [])
188-
189-
const isFieldVisible = useCallback(
190-
(field: ConnectorConfigField): boolean => {
191-
if (!field.canonicalParamId || !field.mode) return true
192-
const activeMode = canonicalModes[field.canonicalParamId] ?? 'basic'
193-
return field.mode === activeMode
194-
},
195-
[canonicalModes]
196-
)
197-
198-
const resolveSourceConfig = useCallback((): Record<string, string> => {
199-
const resolved: Record<string, string> = {}
200-
const processedCanonicals = new Set<string>()
201-
202-
if (!connectorConfig) return resolved
203-
204-
for (const field of connectorConfig.configFields) {
205-
if (field.canonicalParamId) {
206-
if (processedCanonicals.has(field.canonicalParamId)) continue
207-
processedCanonicals.add(field.canonicalParamId)
208-
209-
const group = canonicalGroups.get(field.canonicalParamId)
210-
if (!group) continue
211-
212-
const activeMode = canonicalModes[field.canonicalParamId] ?? 'basic'
213-
const activeField = group.find((f) => f.mode === activeMode) ?? group[0]
214-
const value = sourceConfig[activeField.id]
215-
if (value) resolved[field.canonicalParamId] = value
216-
} else {
217-
if (sourceConfig[field.id]) resolved[field.id] = sourceConfig[field.id]
218-
}
219-
}
220-
221-
return resolved
222-
}, [connectorConfig, canonicalGroups, canonicalModes, sourceConfig])
223-
224127
const canSubmit = useMemo(() => {
225128
if (!connectorConfig) return false
226129
if (isApiKeyMode) {
@@ -249,7 +152,10 @@ export function AddConnectorModal({
249152

250153
setError(null)
251154

252-
const resolvedConfig = resolveSourceConfig()
155+
const resolvedConfig: Record<string, unknown> = {}
156+
for (const [key, value] of Object.entries(resolveSourceConfig())) {
157+
if (value) resolvedConfig[key] = value
158+
}
253159
const finalSourceConfig =
254160
disabledTagIds.size > 0
255161
? { ...resolvedConfig, disabledTagIds: Array.from(disabledTagIds) }
@@ -274,10 +180,6 @@ export function AddConnectorModal({
274180
)
275181
}
276182

277-
const handleConnectNewAccount = useCallback(() => {
278-
setShowOAuthModal(true)
279-
}, [])
280-
281183
const filteredEntries = useMemo(() => {
282184
const term = searchTerm.toLowerCase().trim()
283185
if (!term) return CONNECTOR_ENTRIES
@@ -385,9 +287,7 @@ export function AddConnectorModal({
385287
: `Connect ${connectorConfig.name} account`,
386288
value: '__connect_new__',
387289
icon: Plus,
388-
onSelect: () => {
389-
void handleConnectNewAccount()
390-
},
290+
onSelect: () => setShowOAuthModal(true),
391291
},
392292
]}
393293
value={effectiveCredentialId ?? undefined}

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

File renamed without changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'

0 commit comments

Comments
 (0)