Skip to content

Commit 13faa16

Browse files
committed
refactor(kb-connectors): extract canonical-field logic into useConnectorConfigFields hook
1 parent 958d106 commit 13faa16

File tree

3 files changed

+188
-225
lines changed

3 files changed

+188
-225
lines changed

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

Lines changed: 16 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal
2626
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,61 +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, Set<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) ?? new Set<string>()
126-
existing.add(field.id)
127-
if (field.canonicalParamId) {
128-
for (const sibling of canonicalGroups.get(field.canonicalParamId) ?? []) {
129-
existing.add(sibling.id)
130-
}
131-
}
132-
map.set(dep, existing)
133-
}
134-
}
135-
for (const group of canonicalGroups.values()) {
136-
const allDependents = new Set<string>()
137-
for (const field of group) {
138-
for (const dep of map.get(field.id) ?? []) {
139-
allDependents.add(dep)
140-
const depField = connectorConfig.configFields.find((f) => f.id === dep)
141-
if (depField?.canonicalParamId) {
142-
for (const sibling of canonicalGroups.get(depField.canonicalParamId) ?? []) {
143-
allDependents.add(sibling.id)
144-
}
145-
}
146-
}
147-
}
148-
if (allDependents.size > 0) {
149-
for (const field of group) {
150-
map.set(field.id, new Set(allDependents))
151-
}
152-
}
153-
}
154-
const result = new Map<string, string[]>()
155-
for (const [key, value] of map) result.set(key, [...value])
156-
return result
157-
}, [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 })
158112

159113
const handleSelectType = (type: string) => {
160114
setSelectedType(type)
@@ -170,64 +124,6 @@ export function AddConnectorModal({
170124
onConnectorTypeChange?.(type)
171125
}
172126

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

257153
setError(null)
258154

259-
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+
}
260159
const finalSourceConfig =
261160
disabledTagIds.size > 0
262161
? { ...resolvedConfig, disabledTagIds: Array.from(disabledTagIds) }

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

Lines changed: 19 additions & 108 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 { createLogger } from '@sim/logger'
55
import { ArrowLeftRight, ExternalLink, Loader2, RotateCcw } from 'lucide-react'
66
import {
@@ -26,8 +26,8 @@ import { getSubscriptionAccessState } from '@/lib/billing/client'
2626
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 type { ConnectorData } from '@/hooks/queries/kb/connectors'
@@ -61,11 +61,14 @@ export function EditConnectorModal({
6161
const connectorConfig = CONNECTOR_REGISTRY[connector.connectorType] ?? null
6262

6363
const [activeTab, setActiveTab] = useState('settings')
64+
const [syncInterval, setSyncInterval] = useState(connector.syncIntervalMinutes)
65+
const [error, setError] = useState<string | null>(null)
66+
6467
/**
6568
* Seeds from the stored canonical config. For canonical-pair fields (selector +
6669
* manual input), both field IDs get the same value so toggling preserves it.
6770
*/
68-
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>(() => {
71+
const initialSourceConfig = useMemo(() => {
6972
const config: Record<string, string> = {}
7073
if (!connectorConfig) {
7174
for (const [key, value] of Object.entries(connector.sourceConfig)) {
@@ -80,118 +83,26 @@ export function EditConnectorModal({
8083
if (rawValue !== undefined) config[field.id] = String(rawValue ?? '')
8184
}
8285
return config
83-
})
84-
const [syncInterval, setSyncInterval] = useState(connector.syncIntervalMinutes)
85-
const [error, setError] = useState<string | null>(null)
86-
const [canonicalModes, setCanonicalModes] = useState<Record<string, 'basic' | 'advanced'>>({})
86+
// Seed once on mount; editing state is owned by the hook afterward
87+
// eslint-disable-next-line react-hooks/exhaustive-deps
88+
}, [])
89+
90+
const {
91+
sourceConfig,
92+
canonicalModes,
93+
canonicalGroups,
94+
isFieldVisible,
95+
handleFieldChange,
96+
toggleCanonicalMode,
97+
resolveSourceConfig,
98+
} = useConnectorConfigFields({ connectorConfig, initialSourceConfig })
8799

88100
const { mutate: updateConnector, isPending: isSaving } = useUpdateConnector()
89101

90102
const { data: subscriptionResponse } = useSubscriptionData({ enabled: isBillingEnabled })
91103
const subscriptionAccess = getSubscriptionAccessState(subscriptionResponse?.data)
92104
const hasMaxAccess = !isBillingEnabled || subscriptionAccess.hasUsableMaxAccess
93105

94-
const canonicalGroups = useMemo(() => {
95-
if (!connectorConfig) return new Map<string, ConnectorConfigField[]>()
96-
const groups = new Map<string, ConnectorConfigField[]>()
97-
for (const field of connectorConfig.configFields) {
98-
if (field.canonicalParamId) {
99-
const existing = groups.get(field.canonicalParamId)
100-
if (existing) existing.push(field)
101-
else groups.set(field.canonicalParamId, [field])
102-
}
103-
}
104-
return groups
105-
}, [connectorConfig])
106-
107-
const dependentFieldIds = useMemo(() => {
108-
if (!connectorConfig) return new Map<string, string[]>()
109-
const map = new Map<string, Set<string>>()
110-
for (const field of connectorConfig.configFields) {
111-
const deps = getDependsOnFields(field.dependsOn)
112-
for (const dep of deps) {
113-
const existing = map.get(dep) ?? new Set<string>()
114-
existing.add(field.id)
115-
if (field.canonicalParamId) {
116-
for (const sibling of canonicalGroups.get(field.canonicalParamId) ?? []) {
117-
existing.add(sibling.id)
118-
}
119-
}
120-
map.set(dep, existing)
121-
}
122-
}
123-
for (const group of canonicalGroups.values()) {
124-
const allDependents = new Set<string>()
125-
for (const field of group) {
126-
for (const dep of map.get(field.id) ?? []) {
127-
allDependents.add(dep)
128-
const depField = connectorConfig.configFields.find((f) => f.id === dep)
129-
if (depField?.canonicalParamId) {
130-
for (const sibling of canonicalGroups.get(depField.canonicalParamId) ?? []) {
131-
allDependents.add(sibling.id)
132-
}
133-
}
134-
}
135-
}
136-
if (allDependents.size > 0) {
137-
for (const field of group) map.set(field.id, new Set(allDependents))
138-
}
139-
}
140-
const result = new Map<string, string[]>()
141-
for (const [key, value] of map) result.set(key, [...value])
142-
return result
143-
}, [connectorConfig, canonicalGroups])
144-
145-
const isFieldVisible = (field: ConnectorConfigField): boolean => {
146-
if (!field.canonicalParamId || !field.mode) return true
147-
const activeMode = canonicalModes[field.canonicalParamId] ?? 'basic'
148-
return field.mode === activeMode
149-
}
150-
151-
const handleFieldChange = (fieldId: string, value: string) => {
152-
setSourceConfig((prev) => {
153-
const next = { ...prev, [fieldId]: value }
154-
const toClear = dependentFieldIds.get(fieldId)
155-
if (toClear) {
156-
for (const depId of toClear) next[depId] = ''
157-
}
158-
return next
159-
})
160-
}
161-
162-
const toggleCanonicalMode = (canonicalId: string) => {
163-
setCanonicalModes((prev) => ({
164-
...prev,
165-
[canonicalId]: prev[canonicalId] === 'advanced' ? 'basic' : 'advanced',
166-
}))
167-
}
168-
169-
/**
170-
* Collapse the canonical-pair state back to a flat map keyed by canonical IDs
171-
* (matching what's stored in `connector.sourceConfig`).
172-
*/
173-
const resolveSourceConfig = useCallback((): Record<string, string> => {
174-
const resolved: Record<string, string> = {}
175-
const processedCanonicals = new Set<string>()
176-
if (!connectorConfig) return resolved
177-
178-
for (const field of connectorConfig.configFields) {
179-
if (field.canonicalParamId) {
180-
if (processedCanonicals.has(field.canonicalParamId)) continue
181-
processedCanonicals.add(field.canonicalParamId)
182-
const group = canonicalGroups.get(field.canonicalParamId)
183-
if (!group) continue
184-
const activeMode = canonicalModes[field.canonicalParamId] ?? 'basic'
185-
const activeField = group.find((f) => f.mode === activeMode) ?? group[0]
186-
const value = sourceConfig[activeField.id] ?? ''
187-
resolved[field.canonicalParamId] = value
188-
} else {
189-
resolved[field.id] = sourceConfig[field.id] ?? ''
190-
}
191-
}
192-
return resolved
193-
}, [connectorConfig, canonicalGroups, canonicalModes, sourceConfig])
194-
195106
const hasChanges = useMemo(() => {
196107
if (syncInterval !== connector.syncIntervalMinutes) return true
197108
const resolved = resolveSourceConfig()

0 commit comments

Comments
 (0)