Skip to content

Commit 59a2fcb

Browse files
committed
fix(integrations): keep the workspace gallery until the organization is on the Platform
Workspace Integrations sent every member of an organization with member Search to the organization surface, which lists only Search connections, so they lost the workspace credential gallery. Only an organization on the Platform has an integration center that replaces it, so the host context now reports the platform-org-ui rollout and the link and redirect follow it. Everyone else keeps the workspace routes, as on staging.
1 parent 40344b7 commit 59a2fcb

5 files changed

Lines changed: 109 additions & 22 deletions

File tree

‎apps/sim/lib/api/contracts/workspaces.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ export const workspaceHostContextSchema = z.object({
291291
organizationSearch: z.boolean().optional(),
292292
/** Optional for rolling compatibility with app versions that predate the flag. */
293293
knowledgeMemberAccess: z.boolean().optional(),
294+
/**
295+
* Whether the host organization is on the Platform, whose integration center replaces the
296+
* workspace's. Optional for rolling compatibility; absent keeps Integrations in the workspace.
297+
*/
298+
platformOrgUi: z.boolean().optional(),
294299
/** Optional for rolling compatibility with app versions that predate administrator mode. */
295300
knowledgeSourceMirroredAccess: z.boolean().optional(),
296301
/**
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Failure mode: a workspace member sent to the organization surface, which lists only Search
5+
* connections, loses the workspace credential gallery. Only an organization on the Platform has an
6+
* integration center that replaces it.
7+
*/
8+
import { describe, expect, it } from 'vitest'
9+
import { workspaceIntegrationsPath } from '@/lib/navigation/paths'
10+
11+
const WORKSPACE = '/workspace/ws-1/integrations'
12+
13+
describe('where a workspace’s Integrations lead', () => {
14+
it.each([
15+
['a standalone workspace', { hostOrganizationId: null, features: {} }],
16+
[
17+
'a standalone workspace, even with stale organization features',
18+
{ hostOrganizationId: null, features: { organizationSearch: true, platformOrgUi: true } },
19+
],
20+
[
21+
'an organization with member Search but not on the Platform',
22+
{ hostOrganizationId: 'org-1', features: { organizationSearch: true, platformOrgUi: false } },
23+
],
24+
[
25+
'an app version that predates the Platform field',
26+
{ hostOrganizationId: 'org-1', features: { organizationSearch: true } },
27+
],
28+
[
29+
'a Platform organization the viewer cannot search in',
30+
{ hostOrganizationId: 'org-1', features: { organizationSearch: false, platformOrgUi: true } },
31+
],
32+
['an unresolved host context', null],
33+
])('stay in the workspace for %s', (_name, context) => {
34+
expect(workspaceIntegrationsPath('ws-1', context)).toBe(WORKSPACE)
35+
})
36+
37+
it('lead a member of a Platform organization to its integration center', () => {
38+
expect(
39+
workspaceIntegrationsPath('ws-1', {
40+
hostOrganizationId: 'org-1',
41+
features: { organizationSearch: true, platformOrgUi: true },
42+
})
43+
).toBe('/o/org-1/integrations')
44+
})
45+
})

‎apps/sim/lib/navigation/paths.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,21 @@ export function organizationRoutes(organizationId: string) {
6161

6262
interface WorkspaceIntegrationContext {
6363
hostOrganizationId: string | null
64-
features?: { organizationSearch?: boolean }
64+
features?: { organizationSearch?: boolean; platformOrgUi?: boolean }
6565
}
6666

67-
/** Organization members use the center; standalone workspaces retain their credential routes. */
67+
/**
68+
* Members of an organization on the Platform use its integration center, which replaces the
69+
* workspace's. Everyone else keeps the workspace credential routes: the organization surface lists
70+
* only Search connections, so sending them there would hide their workspace credentials.
71+
*/
6872
export function workspaceIntegrationsPath(
6973
workspaceId: string,
7074
context: WorkspaceIntegrationContext | null
7175
) {
72-
return context?.features?.organizationSearch && context.hostOrganizationId
76+
return context?.features?.organizationSearch &&
77+
context.features.platformOrgUi &&
78+
context.hostOrganizationId
7379
? organizationRoutes(context.hostOrganizationId).integrations
7480
: `/workspace/${workspaceId}/integrations`
7581
}

‎apps/sim/lib/workspaces/host-context.test.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import {
1010
import { permissionsMock, permissionsMockFns } from '@sim/testing/mocks/permissions.mock'
1111
import { beforeEach, describe, expect, it, vi } from 'vitest'
1212

13-
const { mockGetOrganizationSettingsAccess } = vi.hoisted(() => ({
13+
const { mockGetOrganizationSettingsAccess, mockIsPlatformOrgUiEnabled } = vi.hoisted(() => ({
1414
mockGetOrganizationSettingsAccess: vi.fn(),
15+
mockIsPlatformOrgUiEnabled: vi.fn(),
1516
}))
1617

18+
vi.mock('@/lib/platform/rollout', () => ({ isPlatformOrgUiEnabled: mockIsPlatformOrgUiEnabled }))
19+
1720
vi.mock('@/lib/credential-groups/scoped-availability', () => credentialGroupsAvailabilityMock)
1821

1922
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
@@ -77,6 +80,7 @@ describe('getWorkspaceHostContextForViewer', () => {
7780
sourceMirrored: true,
7881
})
7982
mockIsKnowledgeMemberAccessAvailable.mockResolvedValue(true)
83+
mockIsPlatformOrgUiEnabled.mockResolvedValue(false)
8084
})
8185

8286
it.each([
@@ -102,6 +106,7 @@ describe('getWorkspaceHostContextForViewer', () => {
102106
expect(context?.features).toEqual({
103107
credentialGroups: true,
104108
organizationSearch,
109+
platformOrgUi: false,
105110
knowledgeMemberAccess: workspaceKnowledge,
106111
knowledgeSourceMirroredAccess: workspaceKnowledge,
107112
chat: true,
@@ -134,9 +139,29 @@ describe('getWorkspaceHostContextForViewer', () => {
134139
})
135140
expect(context?.hostOrganizationId).toBe('org-host')
136141
expect(context?.features?.organizationSearch).toBe(false)
142+
expect(context?.features?.platformOrgUi).toBe(false)
137143
expect(mockIsKnowledgeMemberAccessAvailable).not.toHaveBeenCalled()
144+
expect(mockIsPlatformOrgUiEnabled).not.toHaveBeenCalled()
138145
})
139146

147+
it.each([true, false])(
148+
'reports the host organization’s Platform rollout (%s) to its members',
149+
async (enabled) => {
150+
mockCheckWorkspaceAccess.mockResolvedValue(accessibleWorkspace('write', 'org-host'))
151+
mockGetOrganizationSettingsAccess.mockResolvedValue({
152+
role: 'member',
153+
isMember: true,
154+
isAdmin: false,
155+
})
156+
mockIsPlatformOrgUiEnabled.mockResolvedValue(enabled)
157+
158+
const context = await getWorkspaceHostContextForViewer('workspace-1', 'member-1')
159+
160+
expect(context?.features?.platformOrgUi).toBe(enabled)
161+
expect(mockIsPlatformOrgUiEnabled).toHaveBeenCalledExactlyOnceWith('org-host')
162+
}
163+
)
164+
140165
it('returns null before loading entitlements when the viewer has no access', async () => {
141166
mockCheckWorkspaceAccess.mockResolvedValue({
142167
exists: true,

‎apps/sim/lib/workspaces/host-context.ts‎

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
resolveKnowledgeAccessAvailability,
1010
} from '@/lib/knowledge/access/availability'
1111
import { getOrganizationSettingsAccess } from '@/lib/organizations/settings-access'
12+
import { isPlatformOrgUiEnabled } from '@/lib/platform/rollout'
1213
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1314

1415
/**
@@ -34,24 +35,28 @@ async function resolveWorkspaceHostContextForViewer(
3435
? getOrganizationSettingsAccess(hostOrganizationId, userId)
3536
: Promise.resolve({ role: null, isMember: false, isAdmin: false }),
3637
])
37-
const [credentialGroupsAvailable, knowledgeAccess, organizationSearch, chat] = await Promise.all([
38-
hostOrganizationId
39-
? isScopedCredentialGroupsAvailable({
40-
kind: 'organization',
41-
organizationId: hostOrganizationId,
42-
})
43-
: Promise.resolve(false),
44-
resolveKnowledgeAccessAvailability({ workspaceId, ownerBilling }),
45-
hostOrganizationId && hostOrganizationAccess.isMember
46-
? isKnowledgeMemberAccessAvailable({ organizationId: hostOrganizationId })
47-
: Promise.resolve(false),
48-
payerHasChatAccess({
49-
billingEntity: ownerBilling.organizationId
50-
? { type: 'organization', id: ownerBilling.organizationId }
51-
: { type: 'user', id: access.workspace.billedAccountUserId },
52-
payerSubscription: { plan: ownerBilling.plan },
53-
}),
54-
])
38+
const [credentialGroupsAvailable, knowledgeAccess, organizationSearch, chat, platformOrgUi] =
39+
await Promise.all([
40+
hostOrganizationId
41+
? isScopedCredentialGroupsAvailable({
42+
kind: 'organization',
43+
organizationId: hostOrganizationId,
44+
})
45+
: Promise.resolve(false),
46+
resolveKnowledgeAccessAvailability({ workspaceId, ownerBilling }),
47+
hostOrganizationId && hostOrganizationAccess.isMember
48+
? isKnowledgeMemberAccessAvailable({ organizationId: hostOrganizationId })
49+
: Promise.resolve(false),
50+
payerHasChatAccess({
51+
billingEntity: ownerBilling.organizationId
52+
? { type: 'organization', id: ownerBilling.organizationId }
53+
: { type: 'user', id: access.workspace.billedAccountUserId },
54+
payerSubscription: { plan: ownerBilling.plan },
55+
}),
56+
hostOrganizationId && hostOrganizationAccess.isMember
57+
? isPlatformOrgUiEnabled(hostOrganizationId)
58+
: Promise.resolve(false),
59+
])
5560

5661
return {
5762
workspace: {
@@ -72,6 +77,7 @@ async function resolveWorkspaceHostContextForViewer(
7277
features: {
7378
credentialGroups: credentialGroupsAvailable,
7479
organizationSearch,
80+
platformOrgUi,
7581
knowledgeMemberAccess: knowledgeAccess.memberScoped,
7682
knowledgeSourceMirroredAccess: knowledgeAccess.sourceMirrored,
7783
chat,

0 commit comments

Comments
 (0)