Skip to content

Commit a144143

Browse files
authored
improvement(org): move the organization view onto the new design system (#9708)
Move the organization view onto the Platform design system: sidebar, Home with Email, Calendar and activity widgets, Integrations, Sim Links, and Search as its own destination, with server prefetch for the first read. Members now land on Home with the Assistant composer. Styles stay scoped under data-platform-ui and check:platform-css enforces the boundary.
1 parent ef35218 commit a144143

388 files changed

Lines changed: 28421 additions & 4413 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.

‎.github/ci-suites.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"group": "organization-search",
2525
"task": "ci:e2e-organization-search"
2626
},
27+
{
28+
"group": "org-redesign",
29+
"task": "ci:e2e-org-redesign"
30+
},
2731
{
2832
"group": "browser-extension-navigation",
2933
"task": "ci:e2e-browser-extension"

‎.github/scripts/ci-plan.gql‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ query($base: String!, $head: String!) {
33
affectedTasks(base: $base, head: $head, tasks: [
44
"test", "build", "type-check", "ci:integration", "ci:desktop-live",
55
"ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox",
6-
"ci:e2e-assistant-actions", "ci:e2e-organization-search",
6+
"ci:e2e-assistant-actions", "ci:e2e-organization-search", "ci:e2e-org-redesign",
77
"ci:e2e-docs-platform",
88
"ci:e2e-browser-extension", "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
99
]) {

‎.github/scripts/http-e2e.sh‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# CI groups, which reuse the production build from `e2e-build`.
44
# The job extracts it into apps/sim/.next before running these suites.
55
#
6-
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo> (run from apps/sim)
6+
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|org-redesign|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo> (run from apps/sim)
77
#
88
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
99
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
@@ -20,7 +20,7 @@
2020
# telemetry flush runs detached and still writes .next/dev).
2121
set -euo pipefail
2222

23-
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo>}
23+
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|org-redesign|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo>}
2424
report_dir="$RUNNER_TEMP/e2e"
2525
ready_timeout_seconds=600
2626
mkdir -p "$report_dir"
@@ -32,6 +32,7 @@ status_log=''
3232
realtime_pid=''
3333
realtime_tag=''
3434
realtime_log=''
35+
agent_pid=''
3536
browsers_pid=''
3637
local_app_url=''
3738
# `dev` or `start`; a group sets it before start_app.
@@ -63,6 +64,10 @@ finish() {
6364
tail -n 100 "$realtime_log"
6465
fi
6566
fi
67+
if [ -n "$agent_pid" ]; then
68+
kill "$agent_pid" 2>/dev/null || true
69+
wait "$agent_pid" 2>/dev/null || true
70+
fi
6671
if [ -n "$browsers_pid" ]; then
6772
kill -- "-$browsers_pid" 2>/dev/null || true
6873
wait "$browsers_pid" 2>/dev/null || true
@@ -133,6 +138,13 @@ start_realtime() {
133138
wait_ready "$realtime_pid" "http://127.0.0.1:$port/health" "$name realtime server"
134139
}
135140

141+
# stop_app: stops the app start_app started, so a group can start it again under other flags.
142+
stop_app() {
143+
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag"
144+
wait "$server_pid" 2>/dev/null || true
145+
server_pid=''
146+
}
147+
136148
# wait_ready <pid> <health-url> <label>
137149
wait_ready() {
138150
local pid=$1 url=$2 label=$3
@@ -268,6 +280,59 @@ case "$group" in
268280
esac
269281
;;
270282

283+
# One hosted Enterprise organization, a scripted worker in place of the
284+
# model, and the app twice: with People off, then on. Hosted is decided by the
285+
# app's hostname, which the job maps to loopback. Both phases report, whichever fails.
286+
org-redesign)
287+
app_server=start
288+
export REDIS_URL=redis://127.0.0.1:6379
289+
export E2E_APP_PUBLIC_URL=http://org-redesign.sim.ai:3029
290+
export NEXT_PUBLIC_APP_URL="$E2E_APP_PUBLIC_URL"
291+
export INTERNAL_API_BASE_URL=http://127.0.0.1:3029 TRUSTED_ORIGINS=http://127.0.0.1:3029
292+
export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3030 SOCKET_SERVER_URL=http://127.0.0.1:3030
293+
export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true
294+
export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true
295+
export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
296+
export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true
297+
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
298+
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true SIM_LINKS=true
299+
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
300+
export INTERNAL_API_SECRET=org-redesign-ci-local-secret-at-least-32-characters
301+
export COPILOT_API_KEY=org-redesign-ci-local-copilot-key MOTHERSHIP_SIM_TRANSPORT=direct
302+
export SIM_AGENT_API_URL=http://127.0.0.1:3031
303+
export GOOGLE_CLIENT_ID=org-redesign-google-client GOOGLE_CLIENT_SECRET=org-redesign-google-secret
304+
export JIRA_CLIENT_ID=org-redesign-jira-client JIRA_CLIENT_SECRET=org-redesign-jira-secret
305+
export CONFLUENCE_CLIENT_ID=org-redesign-confluence-client CONFLUENCE_CLIENT_SECRET=org-redesign-confluence-secret
306+
export STRIPE_SECRET_KEY=sk_test_org_redesign STRIPE_WEBHOOK_SECRET=whsec_org_redesign
307+
export DB_TX_TRIPWIRE=throw
308+
export BROWSER_EXTENSION_IDS=abcdefghijklmnopabcdefghijklmnop
309+
bun --no-env-file scripts/org-redesign-seed.ts
310+
start_browser_install org-redesign --only-shell chromium
311+
bun --no-env-file scripts/org-redesign-e2e/scripted-agent.ts 3031 \
312+
> "$report_dir/org-redesign-agent.log" 2>&1 &
313+
agent_pid=$!
314+
wait_ready "$agent_pid" http://127.0.0.1:3031/health 'scripted agent'
315+
start_app org-redesign 3029 'organization redesign' record-http-status
316+
start_realtime org-redesign 3030
317+
wait_for_browsers
318+
run_status=0
319+
ORG_REDESIGN_E2E_ORIGIN="$E2E_APP_PUBLIC_URL" \
320+
ORG_REDESIGN_E2E_DATABASE_URL="$DATABASE_URL" \
321+
ORG_REDESIGN_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
322+
ORG_REDESIGN_E2E_REPORT_PATH="$report_dir/org-redesign-ui-report.json" \
323+
bun --no-env-file scripts/test-org-redesign-e2e.ts || run_status=$?
324+
stop_app
325+
export PEOPLE=true
326+
start_app org-redesign-people 3029 'organization redesign, People on'
327+
ORG_REDESIGN_E2E_PHASE=people \
328+
ORG_REDESIGN_E2E_ORIGIN="$E2E_APP_PUBLIC_URL" \
329+
ORG_REDESIGN_E2E_DATABASE_URL="$DATABASE_URL" \
330+
ORG_REDESIGN_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
331+
ORG_REDESIGN_E2E_REPORT_PATH="$report_dir/org-redesign-people-ui-report.json" \
332+
bun --no-env-file scripts/test-org-redesign-e2e.ts || run_status=$?
333+
exit "$run_status"
334+
;;
335+
271336
browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access)
272337
extension_group=all
273338
case "$group" in

‎.github/workflows/checks.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ jobs:
207207
needs: [changes, e2e-build]
208208
if: needs.changes.outputs.http == 'true'
209209
runs-on: ubuntu-24.04
210-
timeout-minutes: 20
210+
timeout-minutes: ${{ matrix.group == 'org-redesign' && 45 || 20 }}
211211
strategy:
212212
fail-fast: false
213213
matrix: ${{ fromJSON(needs.changes.outputs.http_matrix) }}
@@ -227,7 +227,7 @@ jobs:
227227
--health-retries 10
228228
# Desktop inbox, browser extension, and Project Files use Redis.
229229
redis:
230-
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'assistant-actions' || matrix.group == 'organization-search' || startsWith(matrix.group, 'browser-extension-') || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
230+
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'assistant-actions' || matrix.group == 'organization-search' || matrix.group == 'org-redesign' || startsWith(matrix.group, 'browser-extension-') || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
231231
ports:
232232
- 6379:6379
233233
options: >-
@@ -293,6 +293,10 @@ jobs:
293293
if: matrix.group == 'scim'
294294
run: printf '127.0.0.1 scim-e2e.sim.ai\n' | sudo tee -a /etc/hosts
295295

296+
- name: Map the hosted organization fixture to loopback
297+
if: matrix.group == 'org-redesign'
298+
run: printf '127.0.0.1 org-redesign.sim.ai\n' | sudo tee -a /etc/hosts
299+
296300
# No step timeout: the job's bound covers a hang without cutting a slow but healthy suite
297301
# short of writing its report.
298302
- name: Run end-to-end suites
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getOrganizationHomeCalendarContract } from '@/lib/api/contracts/organization-home'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import {
8+
organizationHomeOperations,
9+
readOrganizationHomeCalendar,
10+
} from '@/lib/organizations/application/home-widgets'
11+
import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[id]/credential-groups/error-policy'
12+
13+
export const GET = defineInternalJsonRoute({
14+
contract: getOrganizationHomeCalendarContract,
15+
auth: internalSessionAuth,
16+
operation: organizationHomeOperations.readCalendar,
17+
rateLimit: internalRateLimits.user({ bucketName: 'organization-home-calendar' }),
18+
errorPolicy: createCredentialGroupInternalErrorPolicy(
19+
'Failed to load calendar',
20+
'Organization not found'
21+
),
22+
mapInput: ({ params, query }) => ({ organizationId: params.id, ...query }),
23+
useCase: readOrganizationHomeCalendar,
24+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
25+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getOrganizationHomeEmailContract } from '@/lib/api/contracts/organization-home'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import {
8+
organizationHomeOperations,
9+
readOrganizationHomeEmail,
10+
} from '@/lib/organizations/application/home-widgets'
11+
import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[id]/credential-groups/error-policy'
12+
13+
export const GET = defineInternalJsonRoute({
14+
contract: getOrganizationHomeEmailContract,
15+
auth: internalSessionAuth,
16+
operation: organizationHomeOperations.readEmail,
17+
rateLimit: internalRateLimits.user({ bucketName: 'organization-home-email' }),
18+
errorPolicy: createCredentialGroupInternalErrorPolicy(
19+
'Failed to load email',
20+
'Organization not found'
21+
),
22+
mapInput: ({ params }) => ({ organizationId: params.id }),
23+
useCase: readOrganizationHomeEmail,
24+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
25+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { removeSearchHistorySourceContract } from '@/lib/api/contracts/knowledge/search-history'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import {
9+
removeSearchHistorySource,
10+
searchHistoryOperations,
11+
} from '@/lib/knowledge/application/search-history'
12+
13+
export const DELETE = defineInternalJsonRoute({
14+
contract: removeSearchHistorySourceContract,
15+
auth: internalSessionAuth,
16+
operation: searchHistoryOperations.removeSource,
17+
rateLimit: internalRateLimits.user({ bucketName: 'search-history-write' }),
18+
errorPolicy: internalOrchestrationErrorPolicy,
19+
parseOptions: { maxBodyBytes: 24 * 1024 },
20+
mapInput: ({ params, body }) => ({ organizationId: params.id, url: body.url }),
21+
useCase: removeSearchHistorySource,
22+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
23+
})

‎apps/sim/app/api/organizations/[id]/sim-links/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
internalRateLimits,
66
internalSessionAuth,
77
} from '@/lib/api/server/routes'
8-
import { presentSimLink } from '@/lib/api/server/sim-link-presenters'
8+
import { presentSimLink, presentSimLinkPage } from '@/lib/api/server/sim-link-presenters'
99
import { createSimLink, listSimLinks } from '@/lib/sim-links/application/use-cases'
1010

1111
const policy = {
@@ -21,7 +21,7 @@ export const GET = defineInternalJsonRoute({
2121
operation: listSimLinks.operation,
2222
useCase: listSimLinks,
2323
mapInput: ({ params, query }) => ({ organizationId: params.id, ...query }),
24-
present: ({ links, nextCursor }) => ({ links: links.map(presentSimLink), nextCursor }),
24+
present: presentSimLinkPage,
2525
})
2626

2727
export const POST = defineInternalJsonRoute({
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ReactNode } from 'react'
2+
import { type DehydratedState, HydrationBoundary } from '@tanstack/react-query'
3+
4+
interface HydratedProps {
5+
state: Promise<DehydratedState>
6+
children: ReactNode
7+
}
8+
9+
/**
10+
* Children painted from a page's server reads. Put it inside a `Suspense`: it holds only that
11+
* boundary, so the page's arrangement paints before the reads settle.
12+
*/
13+
export async function Hydrated({ state, children }: HydratedProps) {
14+
return <HydrationBoundary state={await state}>{children}</HydrationBoundary>
15+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client'
2+
3+
import '@/platform/design-system/styles/embedded.css'
4+
import type { ReactNode } from 'react'
5+
import { PlatformSurface } from '@/platform/design-system/components/surface/platform-surface'
6+
7+
interface RedesignSurfaceProps {
8+
children: ReactNode
9+
}
10+
11+
/**
12+
* Mounts the design system. `system` follows the app's theme through `<html>`'s class, in CSS, so it
13+
* tracks every change and is right before hydration (`lib/postcss/platform-embed.cjs`).
14+
*
15+
* Never render a legacy component inside: the surface redefines tokens legacy utilities read. Compose
16+
* one beside a surface instead.
17+
*/
18+
export function RedesignSurface({ children }: RedesignSurfaceProps) {
19+
return <PlatformSurface theme='system'>{children}</PlatformSurface>
20+
}

0 commit comments

Comments
 (0)