From 04393ec6a2b292e96ddd0c2400c6c7ef2f2a7a6c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 9 Oct 2026 20:16:56 -0700 Subject: [PATCH 1/2] improvement(app): show branded wordmark while loading --- apps/sim/app/_shell/application-loading.tsx | 36 +++++++++++ apps/sim/app/account/loading.tsx | 5 ++ apps/sim/app/home/loading.tsx | 5 ++ apps/sim/app/o/loading.tsx | 5 ++ apps/sim/app/selfhost/loading.tsx | 5 ++ .../workspace/[workspaceId]/layout.test.tsx | 8 +-- .../app/workspace/[workspaceId]/layout.tsx | 60 +++++++++++++++---- .../app/workspace/[workspaceId]/w/page.tsx | 20 ++----- apps/sim/app/workspace/page.tsx | 17 +----- .../components/sim-wordmark/sim-wordmark.tsx | 16 +++-- scripts/check-unused-exports.baseline.json | 3 - 11 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 apps/sim/app/_shell/application-loading.tsx create mode 100644 apps/sim/app/account/loading.tsx create mode 100644 apps/sim/app/home/loading.tsx create mode 100644 apps/sim/app/o/loading.tsx create mode 100644 apps/sim/app/selfhost/loading.tsx diff --git a/apps/sim/app/_shell/application-loading.tsx b/apps/sim/app/_shell/application-loading.tsx new file mode 100644 index 00000000000..41d6ff7a4b6 --- /dev/null +++ b/apps/sim/app/_shell/application-loading.tsx @@ -0,0 +1,36 @@ +import { cn, SimWordmark } from '@sim/emcn' +import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar' +import { type BrandConfig, getBrandConfig } from '@/ee/whitelabeling' + +interface ApplicationLoadingProps { + brand?: BrandConfig + fullScreen?: boolean +} + +/** Shared loading surface for app entry, shell prefetch, and workflow navigation. */ +export function ApplicationLoading({ + brand = getBrandConfig(), + fullScreen = true, +}: ApplicationLoadingProps) { + const wordmarkUrl = brand.wordmarkUrl || brand.logoUrl + + return ( +
+ {fullScreen && } + {wordmarkUrl ? ( + + ) : brand.isWhitelabeled ? ( + {brand.name} + ) : ( + + )} +
+ ) +} diff --git a/apps/sim/app/account/loading.tsx b/apps/sim/app/account/loading.tsx new file mode 100644 index 00000000000..4d1e787708c --- /dev/null +++ b/apps/sim/app/account/loading.tsx @@ -0,0 +1,5 @@ +import { ApplicationLoading } from '@/app/_shell/application-loading' + +export default function Loading() { + return +} diff --git a/apps/sim/app/home/loading.tsx b/apps/sim/app/home/loading.tsx new file mode 100644 index 00000000000..4d1e787708c --- /dev/null +++ b/apps/sim/app/home/loading.tsx @@ -0,0 +1,5 @@ +import { ApplicationLoading } from '@/app/_shell/application-loading' + +export default function Loading() { + return +} diff --git a/apps/sim/app/o/loading.tsx b/apps/sim/app/o/loading.tsx new file mode 100644 index 00000000000..4d1e787708c --- /dev/null +++ b/apps/sim/app/o/loading.tsx @@ -0,0 +1,5 @@ +import { ApplicationLoading } from '@/app/_shell/application-loading' + +export default function Loading() { + return +} diff --git a/apps/sim/app/selfhost/loading.tsx b/apps/sim/app/selfhost/loading.tsx new file mode 100644 index 00000000000..4d1e787708c --- /dev/null +++ b/apps/sim/app/selfhost/loading.tsx @@ -0,0 +1,5 @@ +import { ApplicationLoading } from '@/app/_shell/application-loading' + +export default function Loading() { + return +} diff --git a/apps/sim/app/workspace/[workspaceId]/layout.test.tsx b/apps/sim/app/workspace/[workspaceId]/layout.test.tsx index b3f50d1d568..0d1a53f8b5a 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.test.tsx @@ -4,7 +4,7 @@ import { createSessionPrincipal } from '@sim/testing/factories/principal.factory import { emcnMock } from '@sim/testing/mocks/emcn.mock' import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock' import { reactQueryMock } from '@sim/testing/mocks/react-query.mock' -import { renderToStaticMarkup } from 'react-dom/server' +import { renderToReadableStream } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -178,7 +178,7 @@ describe('WorkspaceLayout host context', () => { children:
Workspace child
, params: Promise.resolve({ workspaceId: 'workspace-b' }), }) - renderToStaticMarkup(element) + await new Response(await renderToReadableStream(element)).text() expect(mockGetOrgWhitelabelSettings).toHaveBeenCalledWith('org-b') expect(mockGetOrgWhitelabelSettings).not.toHaveBeenCalledWith('org-a') @@ -211,7 +211,7 @@ describe('WorkspaceLayout host context', () => { children:
Secret workspace child
, params: Promise.resolve({ workspaceId: 'workspace-denied' }), }) - const html = renderToStaticMarkup(element) + const html = await new Response(await renderToReadableStream(element)).text() expect(html).toContain('Workspace access denied') expect(html).not.toContain('Secret workspace child') @@ -226,7 +226,7 @@ describe('WorkspaceLayout host context', () => { children:
Workspace child
, params: Promise.resolve({ workspaceId: 'workspace-b' }), }) - renderToStaticMarkup(element) + await new Response(await renderToReadableStream(element)).text() return mockWorkspaceChrome.mock.calls[0][0].sidebar } diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index a7cf24ce571..e3e4d2dc285 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -1,7 +1,9 @@ -import { dehydrate, HydrationBoundary } from '@tanstack/react-query' +import { Suspense } from 'react' +import { dehydrate, HydrationBoundary, type QueryClient } from '@tanstack/react-query' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider' +import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces' import { getSession } from '@/lib/auth' import { getActiveOrganizationId } from '@/lib/auth/session-response' import { isChangelogEnabled } from '@/lib/changelog/feature-flag' @@ -13,6 +15,7 @@ import { import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags' import { resolveOrganizationEntryPath } from '@/lib/navigation/resolve-app-entry' import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag' +import { ApplicationLoading } from '@/app/_shell/application-loading' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner' import { SessionExpired } from '@/app/workspace/[workspaceId]/components/session-expired' @@ -38,16 +41,29 @@ import { WorkspaceHostProvider } from '@/app/workspace/[workspaceId]/providers/w import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/workspace-scope-sync' import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' +import { + getBrandConfig, + mergeOrgBrandConfig, + type OrganizationWhitelabelSettings, +} from '@/ee/whitelabeling' import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider' import { getOrgWhitelabelSettings } from '@/ee/whitelabeling/org-branding' -export default async function WorkspaceLayout({ - children, - params, -}: { +interface WorkspaceLayoutProps { children: React.ReactNode params: Promise<{ workspaceId: string }> -}) { +} + +interface WorkspaceContentProps { + children: React.ReactNode + workspaceId: string + session: NonNullable>> + queryClient: QueryClient + hostContext: WorkspaceHostContext + initialOrgSettings: OrganizationWhitelabelSettings | null +} + +export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) { const session = await getSession() if (!session?.user) { redirect('/login') @@ -60,6 +76,34 @@ export default async function WorkspaceLayout({ return } + const initialOrgSettings = hostContext.hostOrganizationId + ? await getOrgWhitelabelSettings(hostContext.hostOrganizationId) + : null + const brand = mergeOrgBrandConfig(initialOrgSettings, getBrandConfig()) + + return ( + }> + + {children} + + + ) +} + +async function WorkspaceContent({ + children, + workspaceId, + session, + queryClient, + hostContext, + initialOrgSettings, +}: WorkspaceContentProps) { const activeOrganizationId = getActiveOrganizationId(session) const principal = { kind: 'session', @@ -68,7 +112,6 @@ export default async function WorkspaceLayout({ } as const const [ cookieStore, - initialOrgSettings, , modelSelectorEnabled, planModeEnabled, @@ -79,9 +122,6 @@ export default async function WorkspaceLayout({ desktopExecutorRegistered, ] = await Promise.all([ cookies(), - hostContext.hostOrganizationId - ? getOrgWhitelabelSettings(hostContext.hostOrganizationId) - : Promise.resolve(null), prefetchWorkspaceSidebar( queryClient, workspaceId, diff --git a/apps/sim/app/workspace/[workspaceId]/w/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/page.tsx index 5aafcc4815b..3ab2796c8e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/page.tsx @@ -5,29 +5,17 @@ import { Chip } from '@sim/emcn' import { createLogger } from '@sim/logger' import { ReactFlowProvider } from '@xyflow/react' import { useParams, useRouter } from 'next/navigation' +import { ApplicationLoading } from '@/app/_shell/application-loading' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { Panel, Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' import { useWorkflowOperations } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { useOrgBrandConfig } from '@/ee/whitelabeling/components/branding-provider' import { useWorkflows } from '@/hooks/queries/workflows' const logger = createLogger('WorkflowsPage') -function Spinner() { - return ( -
- ) -} - export default function WorkflowsPage() { + const brand = useOrgBrandConfig() const router = useRouter() const params = useParams() const workspaceId = params.workspaceId as string @@ -104,7 +92,7 @@ export default function WorkflowsPage() { )}
) : ( - + )} diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index 4c74c22deb9..bcaa5552339 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -16,6 +16,7 @@ import { isUpgradeReason, UPGRADE_REASON_PARAM, } from '@/lib/billing/upgrade-reasons' +import { ApplicationLoading } from '@/app/_shell/application-loading' import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar' import { useWorkspacesWithMetadata } from '@/hooks/queries/workspace' @@ -213,21 +214,7 @@ export default function WorkspacePage() { ) } - return ( -
- -
-
- ) + return } async function handleWorkflowRedirect( diff --git a/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx b/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx index 88f0e848de7..da58774967d 100644 --- a/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx +++ b/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx @@ -7,18 +7,22 @@ const WORDMARK_FILLS = { 'muted-inverse': 'var(--text-muted-inverse)', } as const +const WORDMARK_SIZES = { + nav: { width: 37, height: 18, className: '-translate-y-[1.5px] h-[18px] w-auto' }, + tag: { width: 21, height: 10, className: 'h-[10px] w-auto' }, + loading: { width: 67, height: 32, className: 'h-8 w-auto' }, +} as const + export interface SimWordmarkProps { - /** Navbar mark or compact mark sized for a 20px ChipTag. */ - size?: 'nav' | 'tag' + /** Navbar, compact 20px ChipTag, or centered application loading mark. */ + size?: keyof typeof WORDMARK_SIZES /** Body ink, inherited foreground, muted brand ink, or light ink for inverse surfaces. */ tone?: keyof typeof WORDMARK_FILLS } /** Canonical Sim logotype, shared by browser and bundled desktop pages. */ export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) { - const compact = size === 'tag' - const width = compact ? 21 : 37 - const height = compact ? 10 : 18 + const { width, height, className } = WORDMARK_SIZES[size] const fill = WORDMARK_FILLS[tone] return ( @@ -28,7 +32,7 @@ export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) { height={height} fill='none' aria-hidden='true' - className={compact ? 'h-[10px] w-auto' : '-translate-y-[1.5px] h-[18px] w-auto'} + className={className} > {WORDMARK_PATHS.map((d) => ( diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index c16098ee35e..75a5ed504e1 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -956,11 +956,8 @@ "apps/sim/ee/whitelabeling/branding.ts#ThemeColors", "apps/sim/ee/whitelabeling/hooks/whitelabel.ts#WHITELABEL_STALE_TIME", "apps/sim/ee/whitelabeling/hooks/whitelabel.ts#whitelabelKeys", - "apps/sim/ee/whitelabeling/index.ts#BrandConfig", - "apps/sim/ee/whitelabeling/index.ts#OrganizationWhitelabelSettings", "apps/sim/ee/whitelabeling/index.ts#ThemeColors", "apps/sim/ee/whitelabeling/index.ts#generateOrgThemeCSS", - "apps/sim/ee/whitelabeling/index.ts#mergeOrgBrandConfig", "apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx#ForkResourcePickerItem", "apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts#MappableMappingKind", "apps/sim/ee/workspace-forking/hooks/background-work.ts#BACKGROUND_WORK_STALE_TIME", From 2f513218cf9cf4aeb0414eef7fdb3234005dce84 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 9 Oct 2026 20:37:51 -0700 Subject: [PATCH 2/2] fix(app): preload workspace data alongside branding --- .../app/workspace/[workspaceId]/layout.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index e3e4d2dc285..e8c733e1647 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -60,7 +60,11 @@ interface WorkspaceContentProps { session: NonNullable>> queryClient: QueryClient hostContext: WorkspaceHostContext - initialOrgSettings: OrganizationWhitelabelSettings | null + orgSettings: Promise +} + +interface WorkspaceLoadingProps { + orgSettings: Promise } export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) { @@ -76,19 +80,18 @@ export default async function WorkspaceLayout({ children, params }: WorkspaceLay return } - const initialOrgSettings = hostContext.hostOrganizationId - ? await getOrgWhitelabelSettings(hostContext.hostOrganizationId) - : null - const brand = mergeOrgBrandConfig(initialOrgSettings, getBrandConfig()) + const orgSettings = hostContext.hostOrganizationId + ? getOrgWhitelabelSettings(hostContext.hostOrganizationId) + : Promise.resolve(null) return ( - }> + }> {children} @@ -96,13 +99,18 @@ export default async function WorkspaceLayout({ children, params }: WorkspaceLay ) } +async function WorkspaceLoading({ orgSettings }: WorkspaceLoadingProps) { + const brand = mergeOrgBrandConfig(await orgSettings, getBrandConfig()) + return +} + async function WorkspaceContent({ children, workspaceId, session, queryClient, hostContext, - initialOrgSettings, + orgSettings, }: WorkspaceContentProps) { const activeOrganizationId = getActiveOrganizationId(session) const principal = { @@ -112,6 +120,7 @@ async function WorkspaceContent({ } as const const [ cookieStore, + initialOrgSettings, , modelSelectorEnabled, planModeEnabled, @@ -122,6 +131,7 @@ async function WorkspaceContent({ desktopExecutorRegistered, ] = await Promise.all([ cookies(), + orgSettings, prefetchWorkspaceSidebar( queryClient, workspaceId,