Skip to content

Commit dafd059

Browse files
committed
Fix reactive subblocks and avoid duplicate credential resolve
1 parent 9e1d103 commit dafd059

File tree

3 files changed

+75
-32
lines changed

3 files changed

+75
-32
lines changed

apps/sim/app/api/auth/oauth/utils.test.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ describe('OAuth Utils', () => {
166166
accountId: 'account-id',
167167
workspaceId: 'workspace-id',
168168
}
169-
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
170169
const mockAccountRow = {
171170
id: 'account-id',
172171
accessToken: 'valid-token',
@@ -176,7 +175,6 @@ describe('OAuth Utils', () => {
176175
userId: 'test-user-id',
177176
}
178177
mockSelectChain([mockResolvedCredential])
179-
mockSelectChain([mockCredentialRow])
180178
mockSelectChain([mockAccountRow])
181179

182180
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
@@ -192,7 +190,6 @@ describe('OAuth Utils', () => {
192190
accountId: 'account-id',
193191
workspaceId: 'workspace-id',
194192
}
195-
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
196193
const mockAccountRow = {
197194
id: 'account-id',
198195
accessToken: 'expired-token',
@@ -202,7 +199,6 @@ describe('OAuth Utils', () => {
202199
userId: 'test-user-id',
203200
}
204201
mockSelectChain([mockResolvedCredential])
205-
mockSelectChain([mockCredentialRow])
206202
mockSelectChain([mockAccountRow])
207203
mockUpdateChain()
208204

@@ -221,7 +217,6 @@ describe('OAuth Utils', () => {
221217

222218
it('should return null if credential not found', async () => {
223219
mockSelectChain([])
224-
mockSelectChain([])
225220

226221
const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
227222

@@ -235,7 +230,6 @@ describe('OAuth Utils', () => {
235230
accountId: 'account-id',
236231
workspaceId: 'workspace-id',
237232
}
238-
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
239233
const mockAccountRow = {
240234
id: 'account-id',
241235
accessToken: 'expired-token',
@@ -245,7 +239,6 @@ describe('OAuth Utils', () => {
245239
userId: 'test-user-id',
246240
}
247241
mockSelectChain([mockResolvedCredential])
248-
mockSelectChain([mockCredentialRow])
249242
mockSelectChain([mockAccountRow])
250243

251244
mockRefreshOAuthToken.mockResolvedValueOnce(null)

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,14 @@ export async function getCredential(requestId: string, credentialId: string, use
231231
return undefined
232232
}
233233

234+
return getCredentialByAccountId(requestId, resolved.accountId, userId)
235+
}
236+
237+
async function getCredentialByAccountId(requestId: string, accountId: string, userId: string) {
234238
const credentials = await db
235239
.select()
236240
.from(account)
237-
.where(and(eq(account.id, resolved.accountId), eq(account.userId, userId)))
241+
.where(and(eq(account.id, accountId), eq(account.userId, userId)))
238242
.limit(1)
239243

240244
if (!credentials.length) {
@@ -244,7 +248,7 @@ export async function getCredential(requestId: string, credentialId: string, use
244248

245249
return {
246250
...credentials[0],
247-
resolvedCredentialId: resolved.accountId,
251+
resolvedCredentialId: accountId,
248252
}
249253
}
250254

@@ -365,8 +369,7 @@ export async function refreshAccessTokenIfNeeded(
365369
return getServiceAccountToken(resolved.credentialId, scopes, impersonateEmail)
366370
}
367371

368-
// Get the credential directly using the getCredential helper
369-
const credential = await getCredential(requestId, credentialId, userId)
372+
const credential = await getCredentialByAccountId(requestId, resolved.accountId, userId)
370373

371374
if (!credential) {
372375
return null

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useMemo } from 'react'
2+
import { useQueries } from '@tanstack/react-query'
23
import {
34
buildCanonicalIndex,
45
evaluateSubBlockCondition,
@@ -7,13 +8,18 @@ import {
78
isSubBlockVisibleForMode,
89
} from '@/lib/workflows/subblocks/visibility'
910
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
10-
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
11+
import { type WorkspaceCredential, workspaceCredentialKeys } from '@/hooks/queries/credentials'
12+
import { fetchJson } from '@/hooks/selectors/helpers'
1113
import { usePermissionConfig } from '@/hooks/use-permission-config'
1214
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
1315
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1416
import { mergeSubblockState } from '@/stores/workflows/utils'
1517
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
1618

19+
interface CredentialResponse {
20+
credential?: WorkspaceCredential | null
21+
}
22+
1723
/**
1824
* Evaluates reactive conditions for subblocks. Always calls the same hooks
1925
* regardless of whether a reactive condition exists (Rules of Hooks).
@@ -26,45 +32,86 @@ function useReactiveConditions(
2632
activeWorkflowId: string | null,
2733
blockSubBlockValues: Record<string, unknown>
2834
): Set<string> {
29-
const reactiveSubBlock = useMemo(
30-
() => subBlocks.find((sb) => sb.reactiveCondition),
35+
const reactiveSubBlocks = useMemo(
36+
() => subBlocks.filter((subBlock) => subBlock.reactiveCondition),
3137
[subBlocks]
3238
)
33-
const reactiveCond = reactiveSubBlock?.reactiveCondition
3439

3540
// Subscribe to watched field values — always called (stable hook count)
36-
const watchedCredentialId = useSubBlockStore(
41+
const watchedCredentialIdsBySubBlock = useSubBlockStore(
3742
useCallback(
3843
(state) => {
39-
if (!reactiveCond || !activeWorkflowId) return ''
44+
if (!activeWorkflowId || reactiveSubBlocks.length === 0) return {}
4045
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] ?? {}
4146
const merged = { ...blockSubBlockValues, ...blockValues }
42-
for (const field of reactiveCond.watchFields) {
43-
const val = merged[field]
44-
if (val && typeof val === 'string') return val
45-
}
46-
return ''
47+
return reactiveSubBlocks.reduce<Record<string, string>>((acc, subBlock) => {
48+
const reactiveCondition = subBlock.reactiveCondition
49+
if (!reactiveCondition) return acc
50+
51+
for (const field of reactiveCondition.watchFields) {
52+
const value = merged[field]
53+
if (value && typeof value === 'string') {
54+
acc[subBlock.id] = value
55+
break
56+
}
57+
}
58+
59+
return acc
60+
}, {})
4761
},
48-
[reactiveCond, activeWorkflowId, blockId, blockSubBlockValues]
62+
[activeWorkflowId, blockId, blockSubBlockValues, reactiveSubBlocks]
4963
)
5064
)
5165

52-
// Always call useWorkspaceCredential (stable hook count), disable when not needed
53-
const { data: credential } = useWorkspaceCredential(
54-
watchedCredentialId || undefined,
55-
Boolean(reactiveCond && watchedCredentialId)
66+
const watchedCredentialIds = useMemo(
67+
() => Array.from(new Set(Object.values(watchedCredentialIdsBySubBlock))),
68+
[watchedCredentialIdsBySubBlock]
5669
)
5770

71+
const credentialQueries = useQueries({
72+
queries: watchedCredentialIds.map((credentialId) => ({
73+
queryKey: workspaceCredentialKeys.detail(credentialId),
74+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
75+
const data = await fetchJson<CredentialResponse>(`/api/credentials/${credentialId}`, {
76+
signal,
77+
})
78+
return data.credential ?? null
79+
},
80+
enabled: Boolean(credentialId) && reactiveSubBlocks.length > 0,
81+
staleTime: 60 * 1000,
82+
})),
83+
})
84+
85+
const credentialTypeById = useMemo(() => {
86+
const typeById = new Map<string, WorkspaceCredential['type']>()
87+
watchedCredentialIds.forEach((credentialId, index) => {
88+
const credential = credentialQueries[index]?.data
89+
if (credential?.type) {
90+
typeById.set(credentialId, credential.type)
91+
}
92+
})
93+
return typeById
94+
}, [credentialQueries, watchedCredentialIds])
95+
5896
return useMemo(() => {
5997
const hidden = new Set<string>()
60-
if (!reactiveSubBlock || !reactiveCond) return hidden
98+
if (reactiveSubBlocks.length === 0) return hidden
6199

62-
const conditionMet = credential?.type === reactiveCond.requiredType
63-
if (!conditionMet) {
64-
hidden.add(reactiveSubBlock.id)
100+
for (const subBlock of reactiveSubBlocks) {
101+
const reactiveCondition = subBlock.reactiveCondition
102+
if (!reactiveCondition) continue
103+
104+
const watchedCredentialId = watchedCredentialIdsBySubBlock[subBlock.id]
105+
const credentialType = watchedCredentialId
106+
? credentialTypeById.get(watchedCredentialId)
107+
: undefined
108+
if (credentialType !== reactiveCondition.requiredType) {
109+
hidden.add(subBlock.id)
110+
}
65111
}
112+
66113
return hidden
67-
}, [reactiveSubBlock, reactiveCond, credential?.type])
114+
}, [credentialTypeById, reactiveSubBlocks, watchedCredentialIdsBySubBlock])
68115
}
69116

70117
/**

0 commit comments

Comments
 (0)