Skip to content

Commit 2a04ef2

Browse files
committed
fix(search): bind HubSpot continuation and isolate setup
1 parent 8852331 commit 2a04ef2

10 files changed

Lines changed: 336 additions & 55 deletions

File tree

‎apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,7 @@ describe('live integrations backend selection', () => {
998998
})
999999
mockUseOrganizationAccounts.mockReturnValue({
10001000
data: {
1001+
availableMcpConnectors: [],
10011002
credentialGroup: {
10021003
status: 'active',
10031004
mcpServers: [],
@@ -1031,6 +1032,7 @@ describe('live integrations backend selection', () => {
10311032
mocks.integrations.mockReturnValue({ data: [{ connectorType: 'slack', approved: true }] })
10321033
mockUseOrganizationAccounts.mockReturnValue({
10331034
data: {
1035+
availableMcpConnectors: [],
10341036
credentialGroup: {
10351037
status: 'active',
10361038
mcpServers: [],

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const group = {
6666
}
6767
const inventory = (overrides = {}) => ({
6868
credentialGroup: group,
69+
availableMcpConnectors: [],
6970
viewerAccounts: [],
7071
viewerMcpAccounts: [],
7172
canManage: false,

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
7575
const available = LIVE_SEARCH_SOURCE_TYPES.filter(
7676
([provider]) =>
7777
LIVE_SEARCH_SCOPE_FIELDS[provider] &&
78+
(provider !== 'hubspot' ||
79+
data.availableMcpConnectors.includes('hubspot') ||
80+
mcpAccounts(provider).length > 0) &&
7881
(approvals.get(provider)?.approved ||
7982
data.viewerAccounts?.some(
8083
(account) => liveSearchProviderForCredential(account.providerId) === provider

‎apps/sim/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal.tsx‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface AddOrganizationSourceModalProps {
2727
type: string
2828
meta: Pick<ConnectorMeta, 'name' | 'icon'> & { auth?: ConnectorMeta['auth'] }
2929
access: { admin: boolean; members: boolean }
30+
availabilityStatus?: 'loading' | 'error'
3031
}[]
3132
pending: boolean
3233
ready: boolean
@@ -52,8 +53,9 @@ export function AddOrganizationSourceModal({
5253
const visible = sources.filter(({ meta }) => meta.name.toLowerCase().includes(query))
5354
const list = (
5455
<div className={RESOURCE_LIST_STACK}>
55-
{visible.map(({ type, meta, access }) => {
56+
{visible.map(({ type, meta, access, availabilityStatus }) => {
5657
const available = access.admin || access.members
58+
const sourceReady = ready && !availabilityStatus
5759
return (
5860
<SettingsResourceRow
5961
key={type}
@@ -67,26 +69,28 @@ export function AddOrganizationSourceModal({
6769
}
6870
title={meta.name}
6971
description={
70-
!ready
71-
? 'Checking availability'
72-
: !available
73-
? 'Unavailable in this deployment'
74-
: (descriptions?.[type] ??
75-
(access.admin
76-
? meta.auth?.mode === 'apiKey'
77-
? 'Connect an API token'
78-
: meta.auth?.mode === 'oauth' &&
79-
meta.auth.adminCredentialType === 'service_account'
80-
? 'Connect a service account'
81-
: 'Connect an admin account'
82-
: type === 'slack'
83-
? 'Set up your Slack app'
84-
: 'Connect member accounts'))
72+
availabilityStatus === 'error'
73+
? 'Could not check availability'
74+
: !sourceReady
75+
? 'Checking availability'
76+
: !available
77+
? 'Unavailable in this deployment'
78+
: (descriptions?.[type] ??
79+
(access.admin
80+
? meta.auth?.mode === 'apiKey'
81+
? 'Connect an API token'
82+
: meta.auth?.mode === 'oauth' &&
83+
meta.auth.adminCredentialType === 'service_account'
84+
? 'Connect a service account'
85+
: 'Connect an admin account'
86+
: type === 'slack'
87+
? 'Set up your Slack app'
88+
: 'Connect member accounts'))
8589
}
86-
disabled={pending || !ready || !available}
90+
disabled={pending || !sourceReady || !available}
8791
onClick={() => onSelect(type, access.admin ? 'admin' : 'members')}
8892
clickLabel={`Set up ${meta.name}`}
89-
navigable={ready && available}
93+
navigable={sourceReady && available}
9094
/>
9195
)
9296
})}

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-settings.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export function LiveSearchSettings() {
8686
).map(([type, meta]) => ({
8787
type,
8888
meta,
89+
availabilityStatus:
90+
type !== 'hubspot' || accounts.isSuccess
91+
? undefined
92+
: accounts.isError
93+
? ('error' as const)
94+
: ('loading' as const),
8995
access: getLiveSearchAccessAvailability(type, availability.integrationAvailability, {
9096
memberAccessAvailable: searchAccess.memberScoped,
9197
availableMcpConnectors: accounts.data?.availableMcpConnectors,
@@ -301,7 +307,7 @@ export function LiveSearchSettings() {
301307
])
302308
)}
303309
pending={update.isPending}
304-
ready={availability.isIntegrationAvailabilityReady && accounts.isSuccess}
310+
ready={availability.isIntegrationAvailabilityReady}
305311
feedback={
306312
accounts.error ? (
307313
<SettingsQueryErrorState

‎apps/sim/app/workspace/[workspaceId]/components/resource-tile/resource-tile.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export function BrandTile({ icon: Icon, background }: BrandTileProps) {
4646
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
4747
style={background ? { background } : undefined}
4848
>
49-
<Icon className={getTileIconColorClass(background)} />
49+
<Icon
50+
className={background ? getTileIconColorClass(background) : 'text-[var(--text-icon)]'}
51+
/>
5052
</div>
5153
)
5254
}

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,22 @@ export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
1919
* Providers whose kind selects a distinct search collection. One query per kind bounds fanout
2020
* while retaining independently searchable collections within the per-account request limit.
2121
*/
22-
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab', 'hubspot'])
22+
const PROVIDER_KIND_SCHEMAS = {
23+
github: z.enum(['issues', 'code', 'repositories', 'commits']),
24+
gitlab: z.enum(['issues', 'code', 'merge_requests', 'wiki']),
25+
hubspot: z.enum(['contacts', 'companies', 'deals', 'tickets']),
26+
} as const
27+
28+
function hasSearchKinds(
29+
provider: LiveSearchProvider
30+
): provider is keyof typeof PROVIDER_KIND_SCHEMAS {
31+
return Object.hasOwn(PROVIDER_KIND_SCHEMAS, provider)
32+
}
2333

2434
const nativeSearchKindSchema = z.enum([
25-
'issues',
26-
'code',
27-
'repositories',
28-
'commits',
29-
'merge_requests',
30-
'wiki',
31-
'contacts',
32-
'companies',
33-
'deals',
34-
'tickets',
35+
...PROVIDER_KIND_SCHEMAS.github.options,
36+
...PROVIDER_KIND_SCHEMAS.gitlab.options,
37+
...PROVIDER_KIND_SCHEMAS.hubspot.options,
3538
])
3639

3740
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
@@ -49,6 +52,15 @@ export const nativeSearchQuerySchema = z
4952
})
5053
.strict()
5154
.superRefine((input, context) => {
55+
if (input.kind && hasSearchKinds(input.provider)) {
56+
const kinds = PROVIDER_KIND_SCHEMAS[input.provider]
57+
if (!kinds.safeParse(input.kind).success)
58+
context.addIssue({
59+
code: 'custom',
60+
path: ['kind'],
61+
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
62+
})
63+
}
5264
if (input.provider === 'notion' && !input.query)
5365
context.addIssue({
5466
code: 'custom',
@@ -81,15 +93,15 @@ export const nativeSearchQueriesSchema = z
8193
}
8294
/** The search a query runs, ignoring its account and any kind its provider does not use. */
8395
const searchKey = ({ accountId: _, kind, ...query }: NativeSearchQuery) =>
84-
JSON.stringify({ ...query, kind: KIND_PROVIDERS.has(query.provider) ? kind : undefined })
96+
JSON.stringify({ ...query, kind: hasSearchKinds(query.provider) ? kind : undefined })
8597
for (const [index, query] of queries.entries()) {
8698
const addIssue = (message: string) =>
8799
context.addIssue({ code: 'custom', path: [index], message })
88100
const earlier = queries.slice(0, index).filter((previous) => overlaps(previous, query))
89101
if (earlier.some((previous) => searchKey(previous) === searchKey(query)))
90102
addIssue('Duplicate native query.')
91103
else if (
92-
KIND_PROVIDERS.has(query.provider) &&
104+
hasSearchKinds(query.provider) &&
93105
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
94106
)
95107
addIssue(

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,22 @@ export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
3636
* Providers whose kind selects a distinct search collection. One query per kind bounds fanout
3737
* while retaining independently searchable collections within the per-account request limit.
3838
*/
39-
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab', 'hubspot'])
39+
const PROVIDER_KIND_SCHEMAS = {
40+
github: z.enum(['issues', 'code', 'repositories', 'commits']),
41+
gitlab: z.enum(['issues', 'code', 'merge_requests', 'wiki']),
42+
hubspot: z.enum(['contacts', 'companies', 'deals', 'tickets']),
43+
} as const
44+
45+
function hasSearchKinds(
46+
provider: LiveSearchProvider
47+
): provider is keyof typeof PROVIDER_KIND_SCHEMAS {
48+
return Object.hasOwn(PROVIDER_KIND_SCHEMAS, provider)
49+
}
4050

4151
const nativeSearchKindSchema = z.enum([
42-
'issues',
43-
'code',
44-
'repositories',
45-
'commits',
46-
'merge_requests',
47-
'wiki',
48-
'contacts',
49-
'companies',
50-
'deals',
51-
'tickets',
52+
...PROVIDER_KIND_SCHEMAS.github.options,
53+
...PROVIDER_KIND_SCHEMAS.gitlab.options,
54+
...PROVIDER_KIND_SCHEMAS.hubspot.options,
5255
])
5356

5457
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
@@ -66,6 +69,15 @@ export const nativeSearchQuerySchema = z
6669
})
6770
.strict()
6871
.superRefine((input, context) => {
72+
if (input.kind && hasSearchKinds(input.provider)) {
73+
const kinds = PROVIDER_KIND_SCHEMAS[input.provider]
74+
if (!kinds.safeParse(input.kind).success)
75+
context.addIssue({
76+
code: 'custom',
77+
path: ['kind'],
78+
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
79+
})
80+
}
6981
if (input.provider === 'notion' && !input.query)
7082
context.addIssue({
7183
code: 'custom',
@@ -98,15 +110,15 @@ export const nativeSearchQueriesSchema = z
98110
}
99111
/** The search a query runs, ignoring its account and any kind its provider does not use. */
100112
const searchKey = ({ accountId: _, kind, ...query }: NativeSearchQuery) =>
101-
JSON.stringify({ ...query, kind: KIND_PROVIDERS.has(query.provider) ? kind : undefined })
113+
JSON.stringify({ ...query, kind: hasSearchKinds(query.provider) ? kind : undefined })
102114
for (const [index, query] of queries.entries()) {
103115
const addIssue = (message: string) =>
104116
context.addIssue({ code: 'custom', path: [index], message })
105117
const earlier = queries.slice(0, index).filter((previous) => overlaps(previous, query))
106118
if (earlier.some((previous) => searchKey(previous) === searchKey(query)))
107119
addIssue('Duplicate native query.')
108120
else if (
109-
KIND_PROVIDERS.has(query.provider) &&
121+
hasSearchKinds(query.provider) &&
110122
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
111123
)
112124
addIssue(

0 commit comments

Comments
 (0)