Skip to content

Commit 232b34e

Browse files
committed
Build organization integrations in the Platform
1 parent 1fde486 commit 232b34e

94 files changed

Lines changed: 3087 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/platform/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sim Platform
22

3-
The new Platform starts here as a standalone Next.js App Router project inside `apps/sim`. Its root opens the product Home screen. Home, Chat and Settings use the new EMCN library. These routes share installed dependencies with Sim and have no database, API, authentication or legacy app providers. The existing Sim app continues to start and build as before.
3+
The new Platform starts here as a standalone Next.js App Router project inside `apps/sim`. Its root opens the product Home screen. Home, Chat, Settings and organization integrations use the new EMCN library. These routes share installed dependencies with Sim and have no database, API, authentication or legacy app providers. The existing Sim app continues to start and build as before.
44

55
To start or resume work in a new Sim chat, say “Use `$implement-platform` to [add, change, consolidate, or remove the UI] on [route].” The [implementation skill](../../../.agents/skills/implement-platform/SKILL.md) reads the [contract](AGENTS.md) and [ownership map](docs/component-ownership.md) for the checkout containing that route. Name the Paper section when implementing an existing design. For a code-first change, describe the desired behavior, where it should apply, and what you want to see on the live page.
66

@@ -11,9 +11,9 @@ bun run --cwd apps/sim dev:platform
1111
SIM_PLATFORM_FIXTURES=1 bun run --cwd apps/sim dev:platform
1212
```
1313

14-
Open [Home](http://localhost:3100/home), [Chat](http://localhost:3100/chat/weekly-review?state=full), [Settings](http://localhost:3100/settings), or the separate [component workbench](http://localhost:3100/workbench). With the flag on, `?state=empty|full|stress` selects the fixture on any of them. `?theme=light|dark|system` selects the product appearance. Product navigation preserves these choices; nothing writes examples to storage. The URL cannot activate the server flag. Old catalog links with a recipe still open the workbench.
14+
Open [Home](http://localhost:3100/home), [Chat](http://localhost:3100/chat/weekly-review?state=full), [Settings](http://localhost:3100/settings), [Integrations](http://localhost:3100/integrations?state=full), or the separate [component workbench](http://localhost:3100/workbench). With the flag on, `?state=empty|full|stress` selects the fixture on any of them. `?theme=light|dark|system` selects the product appearance. Product navigation preserves these choices; nothing writes examples to storage. The URL cannot activate the server flag. Old catalog links with a recipe still open the workbench.
1515

16-
The product routes live under `app/(shell)/`. Their shared App Router layout owns the shell, sidebar scroll viewport, interface preferences and chat edits. Home, Chat and Settings pages render their own bodies inside that layout. Navigating between them keeps the sidebar reading position, including when returning from Settings. The shell selects its gated fixture and appearance from the current URL. Each route owns its components and CSS Modules in a private `_components/<component>/` folder: the [Home chart](app/%28shell%29/home/_components/activity-chart/activity-chart.tsx) sits beside its styles; the [dialog](design-system/components/modal/modal.tsx) belongs to EMCN. Shared shell UI lives in `app/(shell)/_components/`. [Rhythm tokens](design-system/styles/rhythm.css) and the [ownership and interaction contract](AGENTS.md) apply across all three routes. The [Paper state inventory](docs/core-chat-states.md) enumerates the source rows in scope. Integrations, workspace destinations and workspace create/sort return to Home; Wiki is deferred.
16+
The product routes live under `app/(shell)/`. Their shared App Router layout owns the shell, sidebar scroll viewport, interface preferences and chat edits. Home, Chat, Settings and Integrations pages render their own bodies inside that layout. Navigating between them keeps the sidebar reading position, including when returning from Settings. The shell selects its gated fixture and appearance from the current URL. Each route owns its components and CSS Modules in a private `_components/<component>/` folder: the [Home chart](app/%28shell%29/home/_components/activity-chart/activity-chart.tsx) sits beside its styles; the [dialog](design-system/components/modal/modal.tsx) belongs to EMCN. Shared shell UI lives in `app/(shell)/_components/`. [Rhythm tokens](design-system/styles/rhythm.css) and the [ownership and interaction contract](AGENTS.md) apply throughout the product. The [Paper state inventory](docs/core-chat-states.md) enumerates the source rows in scope. The [organization integrations guide](docs/organization-integrations.md) maps the app catalog, Search, account controls and workspace grants to their Phase 3 operations. Workspace destinations and workspace create/sort return to Home. Wiki is deferred.
1717

1818
## Desktop
1919

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const PLATFORM_DESTINATIONS = [
22
{ id: 'home', label: 'Home', icon: 'I104', value: '/home' },
33
{ id: 'settings', label: 'Settings', icon: 'I116', value: '/settings' },
4-
{ id: 'integrations', label: 'Integrations', icon: 'I070', value: '/home' },
4+
{ id: 'integrations', label: 'Integrations', icon: 'I070', value: '/integrations' },
55
] as const

‎apps/sim/platform/app/(shell)/_components/platform-shell/platform-shell.module.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
inset-block-start: var(--shell-edge-inset);
3131
inset-inline-start: var(--ui-row-inset);
3232
}
33-
.homeContent {
33+
.pageContent {
3434
position: relative;
3535
container-type: inline-size;
3636
}

‎apps/sim/platform/app/(shell)/_components/platform-shell/platform-shell.tsx‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export function PlatformShell({ data, children }: PlatformShellProps) {
4141
: undefined
4242
const conversation = pathname.startsWith('/chat')
4343
const activeChat = conversation ? pathname.split('/')[2] : undefined
44-
const home = !settings && !conversation
44+
const home = pathname === '/home'
45+
const integrations = pathname === '/integrations' || pathname.startsWith('/integrations/')
46+
const scrollingPage = home || integrations
4547
const chats = [...chat.created, ...(data?.chats ?? [])]
4648
.filter((item) => !chat.isRemoved(item.id))
4749
.map((item) => ({ ...item, title: chat.titleFor(item.id, item.title) }))
@@ -54,7 +56,9 @@ export function PlatformShell({ data, children }: PlatformShellProps) {
5456
: 'Settings'
5557
: conversation
5658
? chatTitle || 'New chat'
57-
: 'Home'
59+
: integrations
60+
? 'Integrations'
61+
: 'Home'
5862
const drawerOpen = compact && drawer
5963

6064
useEffect(() => {
@@ -105,7 +109,7 @@ export function PlatformShell({ data, children }: PlatformShellProps) {
105109
data={data}
106110
chats={chats}
107111
activeChat={activeChat}
108-
home={home}
112+
pathname={pathname}
109113
settingsSection={section?.key}
110114
actions={(!desktop || compact) && actions}
111115
navigate={navigate}
@@ -135,19 +139,22 @@ export function PlatformShell({ data, children }: PlatformShellProps) {
135139
<WindowHistoryButtons />
136140
{!compact && !collapsed ? actions : openControl}
137141
</div>
138-
{!conversation && !(home && compact) && (
142+
{!conversation && !(scrollingPage && compact) && (
139143
<span className={styles.desktopTitle} data-type='T067'>
140144
<OverflowText text={pageTitle}>{pageTitle}</OverflowText>
141145
</span>
142146
)}
143147
</header>
144148
)}
145149
{!compact && sidebar}
146-
{!home && webOpener}
150+
{!scrollingPage && webOpener}
147151
<div ref={scrollEdges} className={styles.content}>
148-
{home ? (
149-
<ScrollArea label='Home dashboard'>
150-
<div className={styles.homeContent}>
152+
{scrollingPage ? (
153+
<ScrollArea
154+
key={pathname}
155+
label={home ? 'Home dashboard' : 'Organization integrations'}
156+
>
157+
<div className={styles.pageContent}>
151158
{webOpener}
152159
{children}
153160
</div>

‎apps/sim/platform/app/(shell)/_components/product-session.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NotificationSession } from '@/platform/app/(shell)/_components/notifica
88
import { PlatformShell } from '@/platform/app/(shell)/_components/platform-shell/platform-shell'
99
import { ChatChangesProvider } from '@/platform/app/(shell)/_components/session/chat-changes'
1010
import { ComposerDrafts } from '@/platform/app/(shell)/_components/session/composer-drafts'
11+
import { IntegrationChanges } from '@/platform/app/(shell)/_components/session/integration-changes'
1112
import { InterfacePreferences } from '@/platform/app/(shell)/_components/session/interface-preferences'
1213
import { ProductSurface } from '@/platform/app/(shell)/_components/session/product-surface'
1314
import { SidebarLayout } from '@/platform/app/(shell)/_components/sidebar/sidebar-layout'
@@ -30,7 +31,9 @@ export function ProductSession({ children, fixtures }: ProductSessionProps) {
3031
<ChatChangesProvider>
3132
<ComposerDrafts>
3233
<ProductSurface preference={readAppearance(search.get('theme'))}>
33-
<PlatformShell data={fixtures?.[scenario] ?? null}>{children}</PlatformShell>
34+
<IntegrationChanges key={search.get('role') === 'member' ? 'member' : 'admin'}>
35+
<PlatformShell data={fixtures?.[scenario] ?? null}>{children}</PlatformShell>
36+
</IntegrationChanges>
3437
</ProductSurface>
3538
</ComposerDrafts>
3639
</ChatChangesProvider>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client'
2+
3+
import { createContext, type ReactNode, useContext, useState } from 'react'
4+
import {
5+
type IntegrationsData,
6+
memberSearchMethod,
7+
type OrganizationIntegration,
8+
type WorkspaceAccess,
9+
} from '@/platform/lib/integrations/model'
10+
11+
function useEdits() {
12+
const [records, setRecords] = useState<Record<string, OrganizationIntegration | null>>({})
13+
const [grants, setGrants] = useState<Record<string, WorkspaceAccess> | null>(null)
14+
return { records, setRecords, grants, setGrants }
15+
}
16+
17+
const Changes = createContext<ReturnType<typeof useEdits> | null>(null)
18+
19+
interface IntegrationChangesProps {
20+
children: ReactNode
21+
}
22+
23+
export function IntegrationChanges({ children }: IntegrationChangesProps) {
24+
return <Changes.Provider value={useEdits()}>{children}</Changes.Provider>
25+
}
26+
27+
export function useOrganizationIntegrations(data: IntegrationsData | null) {
28+
const changes = useContext(Changes)
29+
if (!changes) throw new Error('Integration controls must be inside the product layout')
30+
const { records, setRecords, grants, setGrants } = changes
31+
const configured = new Map(data?.integrations.map((item) => [item.providerId, item]))
32+
for (const [id, edit] of Object.entries(records)) {
33+
if (edit) configured.set(id, edit)
34+
else configured.delete(id)
35+
}
36+
function save(next: OrganizationIntegration) {
37+
if (!data) return
38+
const current = configured.get(next.providerId)
39+
if (!data.canManage && !current) return
40+
const provider = data.providers.find((entry) => entry.id === next.providerId)
41+
if (!provider) return
42+
const requiredMethod = next.searchApproved && memberSearchMethod(provider)
43+
const updated =
44+
requiredMethod && !next.accountMethods.includes(requiredMethod)
45+
? { ...next, accountMethods: [...next.accountMethods, requiredMethod] }
46+
: next
47+
const previouslyActive = new Set(
48+
current?.people.filter((person) => person.status !== 'revoked').map((person) => person.id)
49+
)
50+
const revoked = new Set(
51+
next.people
52+
.filter((person) => person.status === 'revoked' && previouslyActive.has(person.id))
53+
.map((person) => person.id)
54+
)
55+
setRecords((previous) => {
56+
const updates = { ...previous }
57+
if (data.canManage && revoked.size) {
58+
for (const item of configured.values()) {
59+
if (item.people.some((person) => revoked.has(person.id) && person.status !== 'revoked')) {
60+
updates[item.providerId] = {
61+
...item,
62+
people: item.people.map((person) =>
63+
revoked.has(person.id) ? { ...person, status: 'revoked' } : person
64+
),
65+
}
66+
}
67+
}
68+
}
69+
updates[next.providerId] =
70+
data.canManage || !current ? updated : { ...current, accounts: next.accounts }
71+
return updates
72+
})
73+
}
74+
return {
75+
configured,
76+
grants: grants ?? data?.workspaceGrants ?? {},
77+
save,
78+
remove(id: string) {
79+
if (data?.canManage) setRecords((previous) => ({ ...previous, [id]: null }))
80+
},
81+
setAccess(workspaceId: string, access: WorkspaceAccess | null) {
82+
if (!data?.canManage) return
83+
setGrants((previous) => {
84+
const next = { ...(previous ?? data.workspaceGrants) }
85+
if (access) next[workspaceId] = access
86+
else delete next[workspaceId]
87+
return next
88+
})
89+
},
90+
}
91+
}

‎apps/sim/platform/app/(shell)/_components/sidebar/sidebar.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface SidebarProps {
1818
data: ShellData | null
1919
chats: readonly ShellItem[]
2020
activeChat?: string
21-
home: boolean
21+
pathname: string
2222
settingsSection?: string
2323
actions?: ReactNode
2424
navigate: (href: string) => void
@@ -28,7 +28,7 @@ export function Sidebar({
2828
data,
2929
chats,
3030
activeChat,
31-
home,
31+
pathname,
3232
settingsSection,
3333
actions,
3434
navigate,
@@ -73,7 +73,7 @@ export function Sidebar({
7373
<Chip
7474
key={item.id}
7575
href={platformHref(item.value, search)}
76-
selected={home && item.id === 'home'}
76+
selected={pathname === item.value || pathname.startsWith(`${item.value}/`)}
7777
icon={item.icon}
7878
>
7979
{item.label}

‎apps/sim/platform/app/(shell)/home/_components/activity-list/activity-list.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useSearchParams } from 'next/navigation'
12
import styles from '@/platform/app/(shell)/home/_components/activity-list/activity-list.module.css'
23
import { HomeSection } from '@/platform/app/(shell)/home/_components/home-section/home-section'
34
import type { HomeData } from '@/platform/app/(shell)/home/_components/model'
@@ -8,6 +9,7 @@ import {
89
Icon,
910
ScrollArea,
1011
} from '@/platform/design-system/components'
12+
import { platformHref } from '@/platform/design-system/navigation'
1113
import { formatRelativeTime } from '@/platform/lib/time'
1214

1315
interface ActivityListProps {
@@ -39,7 +41,8 @@ export function EmailList({ data, onOpen }: ActivityListProps) {
3941
)
4042
}
4143

42-
export function AttentionList({ data, onOpen }: ActivityListProps) {
44+
export function AttentionList({ data }: Pick<ActivityListProps, 'data'>) {
45+
const search = useSearchParams()
4346
return (
4447
<HomeSection title='Needs your attention'>
4548
{data?.attention.length ? (
@@ -53,7 +56,7 @@ export function AttentionList({ data, onOpen }: ActivityListProps) {
5356
title={row.title}
5457
detail={row.detail}
5558
metadata={formatRelativeTime(row.occurredAt, data.asOf)}
56-
onOpen={onOpen}
59+
href={platformHref(row.href ?? '/home', search)}
5760
/>
5861
))}
5962
</ul>

‎apps/sim/platform/app/(shell)/home/_components/home-dashboard/home-dashboard.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function HomeDashboard({ data, recentLinks }: HomeDashboardProps) {
127127
{connectedWidget('Email')}
128128
</div>
129129
<div className={styles.column}>
130-
<AttentionList data={data} onOpen={goHome} />
130+
<AttentionList data={data} />
131131
{connectedWidget('Calendar')}
132132
</div>
133133
</div>

‎apps/sim/platform/app/(shell)/home/_components/model.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface AttentionItem {
77
detail: string
88
occurredAt: number
99
icon: IconName
10+
href?: string
1011
}
1112

1213
export interface HomeData {

0 commit comments

Comments
 (0)