From 46ba40af076ba5c8ede0be857ee376839e038f86 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 24 Jul 2026 18:40:33 -0700 Subject: [PATCH 1/5] refactor(settings): fold verified domains into SSO and use shared primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified domains only gates SSO, so managing it on a separate page meant discovering the requirement after filling out the whole IdP form and then navigating away mid-setup. Move it into the SSO page as a section above the provider config and drop the standalone page, its nav entry, and both route branches. Align the surfaces with the shared settings primitives rather than bespoke chrome, matching whitelabeling/custom-blocks/access-control: - SSO's local FormField (muted labels) is replaced by the shared SettingRow, so its fields read like every other settings page. SettingRow gains optional `optional` and `error` props to absorb what FormField did — additive, so existing consumers are untouched. - The domains section is built from SettingsSection, SettingRow, SettingsResourceRow, and SettingsEmptyState instead of hand-rolled cards. Also drop the redundant Upload/Change buttons in whitelabeling: the logo and wordmark thumbnails were already clickable, so the button was a second control for the same action. Remove still appears once an image is set. --- .../platform/enterprise/verified-domains.mdx | 2 +- apps/sim/app/api/auth/sso/register/route.ts | 2 +- .../settings/[section]/settings.tsx | 6 - .../[workspaceId]/settings/navigation.test.ts | 1 - .../components/settings/navigation.test.ts | 3 - apps/sim/components/settings/navigation.ts | 28 +- .../organization-settings-renderer.tsx | 6 - apps/sim/ee/components/setting-row.tsx | 19 +- .../ee/sso/components/sso-settings.test.tsx | 6 + apps/sim/ee/sso/components/sso-settings.tsx | 737 +++++++++--------- ...tings.tsx => verified-domains-section.tsx} | 135 ++-- .../components/whitelabeling-settings.tsx | 46 +- 12 files changed, 467 insertions(+), 524 deletions(-) rename apps/sim/ee/sso/components/{domain-settings.tsx => verified-domains-section.tsx} (60%) diff --git a/apps/docs/content/docs/en/platform/enterprise/verified-domains.mdx b/apps/docs/content/docs/en/platform/enterprise/verified-domains.mdx index fe91e17e83e..5c7464b6090 100644 --- a/apps/docs/content/docs/en/platform/enterprise/verified-domains.mdx +++ b/apps/docs/content/docs/en/platform/enterprise/verified-domains.mdx @@ -16,7 +16,7 @@ Verified Domains let organization owners and admins on Enterprise plans prove th ## Verify a domain -Go to **Settings → Security → Verified domains** in your organization settings. +Go to **Settings → Security → Single sign-on** in your organization settings. Domains are managed in the **Verified domains** section at the top of that page, directly above the identity provider configuration. 1. Enter the domain, for example `acme.com`, and click **Add domain**. 2. Sim shows a DNS **TXT record** to publish — a host (`_sim-challenge.acme.com`) and a unique value (`sim-domain-verification=…`). diff --git a/apps/sim/app/api/auth/sso/register/route.ts b/apps/sim/app/api/auth/sso/register/route.ts index 23f872c5077..0c78d76f216 100644 --- a/apps/sim/app/api/auth/sso/register/route.ts +++ b/apps/sim/app/api/auth/sso/register/route.ts @@ -148,7 +148,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const domainNotVerifiedResponse = () => NextResponse.json( { - error: `Verify ownership of ${domain} under Settings → Verified domains before configuring SSO for it.`, + error: `Verify ownership of ${domain} under Verified domains above before configuring SSO for it.`, code: 'SSO_DOMAIN_NOT_VERIFIED', }, { status: 403 } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index 5d2a80a52e6..cf2bf5caf18 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -81,9 +81,6 @@ const AuditLogs = dynamic(() => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs) ) const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO)) -const DomainSettings = dynamic(() => - import('@/ee/sso/components/domain-settings').then((m) => m.DomainSettings) -) const SessionPolicySettings = dynamic(() => import('@/ee/session-policy/components/session-policy-settings').then( (m) => m.SessionPolicySettings @@ -166,9 +163,6 @@ export function SettingsPage({ section }: SettingsPageProps) { /> )} {effectiveSection === 'sso' && organizationId && } - {effectiveSection === 'domains' && organizationId && ( - - )} {effectiveSection === 'sessions' && organizationId && ( )} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts index 2996eb3c238..fc2e914762b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts @@ -39,7 +39,6 @@ describe('unified settings navigation', () => { { id: 'inbox', label: 'Sim mailer', section: 'system' }, { id: 'recently-deleted', label: 'Recently deleted', section: 'system' }, { id: 'sso', label: 'Single sign-on', section: 'enterprise' }, - { id: 'domains', label: 'Verified domains', section: 'enterprise' }, { id: 'sessions', label: 'Session policies', section: 'enterprise' }, { id: 'data-retention', label: 'Data retention', section: 'enterprise' }, { id: 'data-drains', label: 'Data drains', section: 'enterprise' }, diff --git a/apps/sim/components/settings/navigation.test.ts b/apps/sim/components/settings/navigation.test.ts index 15cd1c21ed4..3693f88fd48 100644 --- a/apps/sim/components/settings/navigation.test.ts +++ b/apps/sim/components/settings/navigation.test.ts @@ -42,7 +42,6 @@ describe('settings navigation boundaries', () => { 'inbox', 'recently-deleted', 'sso', - 'domains', 'sessions', 'data-retention', 'data-drains', @@ -65,7 +64,6 @@ describe('settings navigation boundaries', () => { 'access-control', 'audit-logs', 'sso', - 'domains', 'sessions', 'data-retention', 'data-drains', @@ -121,7 +119,6 @@ describe('settings navigation boundaries', () => { 'billing', 'data-drains', 'data-retention', - 'domains', 'organization', 'sessions', 'sso', diff --git a/apps/sim/components/settings/navigation.ts b/apps/sim/components/settings/navigation.ts index c8eab37aea1..acf5ac6c8b6 100644 --- a/apps/sim/components/settings/navigation.ts +++ b/apps/sim/components/settings/navigation.ts @@ -6,7 +6,6 @@ import { HexSimple, Key, KeySquare, - Link, Lock, LogIn, Palette, @@ -43,7 +42,6 @@ export type OrganizationSettingsSection = | 'access-control' | 'audit-logs' | 'sso' - | 'domains' | 'sessions' | 'data-retention' | 'data-drains' @@ -90,7 +88,6 @@ export type UnifiedSettingsSection = | 'teammates' | 'organization' | 'sso' - | 'domains' | 'whitelabeling' | 'copilot' | 'forks' @@ -544,22 +541,6 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] organization: { id: 'sso', group: 'security', order: 4 }, }, }, - { - label: 'Verified domains', - icon: Link, - docsLink: 'https://docs.sim.ai/platform/enterprise/verified-domains', - unified: { - id: 'domains', - description: 'Prove ownership of your email domains before configuring SSO.', - group: 'enterprise', - requiresHosted: true, - requiresEnterprise: true, - selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sso, - }, - planes: { - organization: { id: 'domains', group: 'security', order: 5 }, - }, - }, { label: 'Session policies', icon: Clock, @@ -573,7 +554,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies, }, planes: { - organization: { id: 'sessions', group: 'security', order: 6 }, + organization: { id: 'sessions', group: 'security', order: 5 }, }, }, { @@ -590,7 +571,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention, }, planes: { - organization: { id: 'data-retention', group: 'enterprise', order: 7 }, + organization: { id: 'data-retention', group: 'enterprise', order: 6 }, }, }, { @@ -606,7 +587,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains, }, planes: { - organization: { id: 'data-drains', group: 'enterprise', order: 8 }, + organization: { id: 'data-drains', group: 'enterprise', order: 7 }, }, }, { @@ -622,7 +603,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.whitelabeling, }, planes: { - organization: { id: 'whitelabeling', group: 'enterprise', order: 9 }, + organization: { id: 'whitelabeling', group: 'enterprise', order: 8 }, }, }, { @@ -758,7 +739,6 @@ export function getOrganizationSettingsFeatures( 'access-control': SETTINGS_SELF_HOSTED_OVERRIDES.accessControl, 'audit-logs': SETTINGS_SELF_HOSTED_OVERRIDES.auditLogs, sso: SETTINGS_SELF_HOSTED_OVERRIDES.sso, - domains: SETTINGS_SELF_HOSTED_OVERRIDES.sso, sessions: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies, 'data-retention': SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention, 'data-drains': SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains, diff --git a/apps/sim/components/settings/organization-settings-renderer.tsx b/apps/sim/components/settings/organization-settings-renderer.tsx index d2a3e0d6324..66ae7a1efc3 100644 --- a/apps/sim/components/settings/organization-settings-renderer.tsx +++ b/apps/sim/components/settings/organization-settings-renderer.tsx @@ -23,9 +23,6 @@ const AuditLogs = dynamic(() => import('@/ee/audit-logs/components/audit-logs').then((module) => module.AuditLogs) ) const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((module) => module.SSO)) -const DomainSettings = dynamic(() => - import('@/ee/sso/components/domain-settings').then((module) => module.DomainSettings) -) const SessionPolicySettings = dynamic(() => import('@/ee/session-policy/components/session-policy-settings').then( (module) => module.SessionPolicySettings @@ -71,9 +68,6 @@ export function OrganizationSettingsRenderer({ } if (section === 'audit-logs') return if (section === 'sso') return - if (section === 'domains') { - return - } if (section === 'sessions') { return } diff --git a/apps/sim/ee/components/setting-row.tsx b/apps/sim/ee/components/setting-row.tsx index 4d854cd908a..dc95d015735 100644 --- a/apps/sim/ee/components/setting-row.tsx +++ b/apps/sim/ee/components/setting-row.tsx @@ -6,14 +6,28 @@ interface SettingRowProps { description?: string /** Optional supplementary guidance shown in a tooltip on an info icon beside the label. */ labelTooltip?: string + /** Marks the field as not required, rendered as a muted suffix on the label. */ + optional?: boolean + /** Validation message rendered beneath the control. */ + error?: React.ReactNode children: React.ReactNode } -export function SettingRow({ label, description, labelTooltip, children }: SettingRowProps) { +export function SettingRow({ + label, + description, + labelTooltip, + optional = false, + error, + children, +}: SettingRowProps) { return (
- + {labelTooltip && ( @@ -27,6 +41,7 @@ export function SettingRow({ label, description, labelTooltip, children }: Setti
{description &&

{description}

} {children} + {error ?

{error}

: null}
) } diff --git a/apps/sim/ee/sso/components/sso-settings.test.tsx b/apps/sim/ee/sso/components/sso-settings.test.tsx index 9ce0f36f06b..0f584bba706 100644 --- a/apps/sim/ee/sso/components/sso-settings.test.tsx +++ b/apps/sim/ee/sso/components/sso-settings.test.tsx @@ -51,6 +51,12 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: mockUseSession, })) +// Domain management is covered by its own tests and needs a QueryClient; this +// suite only exercises the provider form's org-transition behavior. +vi.mock('@/ee/sso/components/verified-domains-section', () => ({ + VerifiedDomainsSection: () =>
, +})) + vi.mock( '@/app/workspace/[workspaceId]/settings/components/save-discard-actions/save-discard-actions', () => ({ diff --git a/apps/sim/ee/sso/components/sso-settings.tsx b/apps/sim/ee/sso/components/sso-settings.tsx index df9e1a586f8..6ac9233cdb5 100644 --- a/apps/sim/ee/sso/components/sso-settings.tsx +++ b/apps/sim/ee/sso/components/sso-settings.tsx @@ -1,6 +1,6 @@ 'use client' -import { type ReactNode, useState } from 'react' +import { useState } from 'react' import { Button, ChipCombobox, @@ -10,7 +10,6 @@ import { cn, Expandable, ExpandableContent, - Label, Switch, toast, } from '@sim/emcn' @@ -26,37 +25,16 @@ import { saveDiscardActions } from '@/app/workspace/[workspaceId]/settings/compo import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' +import { SettingRow } from '@/ee/components/setting-row' +import { VerifiedDomainsSection } from '@/ee/sso/components/verified-domains-section' import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants' import { useConfigureSSO, useSSOProviders } from '@/ee/sso/hooks/sso' import { useOrganizationBilling } from '@/hooks/queries/organization' const logger = createLogger('SSO') -interface FormFieldProps { - label: ReactNode - children: ReactNode - optional?: boolean - error?: ReactNode -} - -/** - * Page-level labeled-field row for the SSO settings form, matching the - * standalone-field rhythm: muted label, control, then a caption-sized error. - */ -function FormField({ label, children, optional = false, error }: FormFieldProps) { - return ( -
- - {children} - {error ?

{error}

: null} -
- ) -} - interface SSOProvider { id: string providerId: string @@ -420,52 +398,56 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { return ( -
- -

{existingProvider.providerId}

-
- - -

- {existingProvider.providerType.toUpperCase()} -

-
- - -

{existingProvider.domain}

-
- - -

- {existingProvider.issuer} -

-
- - - copyToClipboard(providerCallbackUrl)} - className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' - aria-label='Copy callback URL' - > - {copied ? ( - - ) : ( - - )} - - } - /> -

- Configure this in your identity provider -

-
-
+ + + +
+ +

{existingProvider.providerId}

+
+ + +

+ {existingProvider.providerType.toUpperCase()} +

+
+ + +

{existingProvider.domain}

+
+ + +

+ {existingProvider.issuer} +

+
+ + + copyToClipboard(providerCallbackUrl)} + className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' + aria-label='Copy callback URL' + > + {copied ? ( + + ) : ( + + )} + + } + /> +

+ Configure this in your identity provider +

+
+
+
) } @@ -520,312 +502,321 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { }), ]} > -
- - - handleInputChange('providerType', value as 'oidc' | 'saml') - } - options={[ - { label: 'OIDC', value: 'oidc' }, - { label: 'SAML', value: 'saml' }, - ]} - placeholder='Select provider type' - /> -

- {formData.providerType === 'oidc' - ? 'OpenID Connect (Okta, Azure AD, Auth0, etc.)' - : 'Security Assertion Markup Language (ADFS, Shibboleth, etc.)'} -

-
- - 0 ? errors.providerId.join(' ') : undefined - } - > - handleInputChange('providerId', value)} - options={SSO_TRUSTED_PROVIDERS.map((id) => ({ - label: id, - value: id, - }))} - placeholder='Select or enter a provider ID' - editable - /> - - - 0 ? errors.issuerUrl.join(' ') : undefined - } - > - e.target.removeAttribute('readOnly')} - onChange={(e) => handleInputChange('issuerUrl', e.target.value)} - error={showErrors && errors.issuerUrl.length > 0} - /> - - - 0 ? errors.domain.join(' ') : undefined} - > - e.target.removeAttribute('readOnly')} - onChange={(e) => handleInputChange('domain', e.target.value)} - error={showErrors && errors.domain.length > 0} - /> -

- The email domain users sign in with (e.g. company.com) -

-
- - {formData.providerType === 'oidc' ? ( - <> - 0 ? errors.clientId.join(' ') : undefined + + + +
+ + + handleInputChange('providerType', value as 'oidc' | 'saml') } - > - e.target.removeAttribute('readOnly')} - onChange={(e) => handleInputChange('clientId', e.target.value)} - error={showErrors && errors.clientId.length > 0} - /> - - - 0 - ? errors.clientSecret.join(' ') - : undefined - } - > - { - e.target.removeAttribute('readOnly') - setShowClientSecret(true) - }} - onBlurCapture={() => setShowClientSecret(false)} - onChange={(e) => handleInputChange('clientSecret', e.target.value)} - inputClassName={!showClientSecret ? '[-webkit-text-security:disc]' : undefined} - error={showErrors && errors.clientSecret.length > 0} - endAdornment={ - + options={[ + { label: 'OIDC', value: 'oidc' }, + { label: 'SAML', value: 'saml' }, + ]} + placeholder='Select provider type' + /> +

+ {formData.providerType === 'oidc' + ? 'OpenID Connect (Okta, Azure AD, Auth0, etc.)' + : 'Security Assertion Markup Language (ADFS, Shibboleth, etc.)'} +

+
+ + 0 ? errors.providerId.join(' ') : undefined + } + > + handleInputChange('providerId', value)} + options={SSO_TRUSTED_PROVIDERS.map((id) => ({ + label: id, + value: id, + }))} + placeholder='Select or enter a provider ID' + editable + /> + + + 0 ? errors.issuerUrl.join(' ') : undefined + } + > + e.target.removeAttribute('readOnly')} + onChange={(e) => handleInputChange('issuerUrl', e.target.value)} + error={showErrors && errors.issuerUrl.length > 0} + /> + + + 0 ? errors.domain.join(' ') : undefined} + > + e.target.removeAttribute('readOnly')} + onChange={(e) => handleInputChange('domain', e.target.value)} + error={showErrors && errors.domain.length > 0} + /> +

+ The email domain users sign in with (e.g. company.com) +

+
+ + {formData.providerType === 'oidc' ? ( + <> + 0 ? errors.clientId.join(' ') : undefined } - /> - - - 0 ? errors.scopes.join(' ') : undefined} - > - handleInputChange('scopes', e.target.value)} - error={showErrors && errors.scopes.length > 0} - /> -

- Comma-separated list of OIDC scopes to request -

-
- - ) : ( - <> - 0 - ? errors.entryPoint.join(' ') - : undefined - } - > - handleInputChange('entryPoint', e.target.value)} - error={showErrors && errors.entryPoint.length > 0} - /> - - - 0 ? errors.cert.join(' ') : undefined} - > - handleInputChange('cert', e.target.value)} - className='min-h-[80px] font-mono' - error={showErrors && errors.cert.length > 0} - rows={3} - /> - - -
- - - - -
- - handleInputChange('audience', e.target.value)} - /> - - - - handleInputChange('callbackUrl', e.target.value)} - /> - - - - - handleInputChange('wantAssertionsSigned', checked) - } - /> - - - - handleInputChange('idpMetadata', e.target.value)} - className='min-h-[60px] font-mono' - rows={2} - /> - -
-
-
-
- - )} - - - copyToClipboard(callbackUrl)} - className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' - aria-label='Copy callback URL' +
+ + 0 + ? errors.clientSecret.join(' ') + : undefined + } > - {copied ? ( - - ) : ( - - )} - - } - /> -

- Configure this in your identity provider -

- -
+ { + e.target.removeAttribute('readOnly') + setShowClientSecret(true) + }} + onBlurCapture={() => setShowClientSecret(false)} + onChange={(e) => handleInputChange('clientSecret', e.target.value)} + inputClassName={!showClientSecret ? '[-webkit-text-security:disc]' : undefined} + error={showErrors && errors.clientSecret.length > 0} + endAdornment={ + + } + /> + + + 0 ? errors.scopes.join(' ') : undefined + } + > + handleInputChange('scopes', e.target.value)} + error={showErrors && errors.scopes.length > 0} + /> +

+ Comma-separated list of OIDC scopes to request +

+
+ + ) : ( + <> + 0 + ? errors.entryPoint.join(' ') + : undefined + } + > + handleInputChange('entryPoint', e.target.value)} + error={showErrors && errors.entryPoint.length > 0} + /> + + + 0 ? errors.cert.join(' ') : undefined} + > + handleInputChange('cert', e.target.value)} + className='min-h-[80px] font-mono' + error={showErrors && errors.cert.length > 0} + rows={3} + /> + + +
+ + + + +
+ + handleInputChange('audience', e.target.value)} + /> + + + + handleInputChange('callbackUrl', e.target.value)} + /> + + + + + handleInputChange('wantAssertionsSigned', checked) + } + /> + + + + handleInputChange('idpMetadata', e.target.value)} + className='min-h-[60px] font-mono' + rows={2} + /> + +
+
+
+
+ + )} + + + copyToClipboard(callbackUrl)} + className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' + aria-label='Copy callback URL' + > + {copied ? ( + + ) : ( + + )} + + } + /> +

+ Configure this in your identity provider +

+
+
+ ) diff --git a/apps/sim/ee/sso/components/domain-settings.tsx b/apps/sim/ee/sso/components/verified-domains-section.tsx similarity index 60% rename from apps/sim/ee/sso/components/domain-settings.tsx rename to apps/sim/ee/sso/components/verified-domains-section.tsx index 04f10e6a8d3..43efc05147a 100644 --- a/apps/sim/ee/sso/components/domain-settings.tsx +++ b/apps/sim/ee/sso/components/verified-domains-section.tsx @@ -2,11 +2,14 @@ import { useState } from 'react' import { Button, ChipConfirmModal, ChipCopyInput, ChipInput, ChipTag, toast } from '@sim/emcn' +import { Link } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' import type { OrganizationDomain } from '@/lib/api/contracts/organization' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' -import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { SettingRow } from '@/ee/components/setting-row' import { useAddOrganizationDomain, useOrganizationDomains, @@ -14,26 +17,10 @@ import { useVerifyOrganizationDomain, } from '@/ee/sso/hooks/domains' -interface DomainSettingsProps { +interface VerifiedDomainsSectionProps { organizationId: string } -interface CopyFieldProps { - label: string - value: string - hint?: string -} - -function CopyField({ label, value, hint }: CopyFieldProps) { - return ( -
- {label} - - {hint ? {hint} : null} -
- ) -} - interface DomainRowProps { organizationId: string domain: OrganizationDomain @@ -42,6 +29,7 @@ interface DomainRowProps { function DomainRow({ organizationId, domain, onRemove }: DomainRowProps) { const verifyDomain = useVerifyOrganizationDomain() + const isVerified = domain.status === 'verified' async function handleVerify() { try { @@ -53,32 +41,45 @@ function DomainRow({ organizationId, domain, onRemove }: DomainRowProps) { } return ( -
-
- {domain.domain} -
- - {domain.status === 'verified' ? 'Verified' : 'Pending'} - - onRemove(domain), destructive: true }]} - /> -
-
- - {domain.status === 'pending' && domain.txtRecordValue && ( -
-

- Add this TXT record at your DNS provider, then verify. DNS changes can take up to 48 - hours to propagate. -

- + } + title={domain.domain} + description={isVerified ? 'Ownership verified' : 'Awaiting DNS verification'} + trailing={ +
+ + {isVerified ? 'Verified' : 'Pending'} + + onRemove(domain), destructive: true }]} + /> +
+ } + /> + + {!isVerified && domain.txtRecordValue && ( +
+ - + description='Some DNS providers append your zone automatically. If yours does, enter this host with the trailing zone removed.' + > + + + + + + +
- {logoUpload.previewUrl && ( - - )} -
+ )} -
+ {wordmarkUpload.previewUrl && ( - {wordmarkUpload.previewUrl && ( - - )} -
+ )} Date: Fri, 24 Jul 2026 18:48:30 -0700 Subject: [PATCH 2/5] fix(settings): move group-detail view state to nuqs and clean up design-system drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Access control's group detail kept its tab, three search boxes, and three status filters in useState, so a `?group-id=` link always landed on General and a filter was lost on reload — the parent already puts the group id in the URL. The three tabs never render together, so search and status share one param each rather than carrying three mutually-exclusive keys, and switching tabs resets both. Closing the detail clears all three alongside group-id in one batched write, so nothing lingers on the list URL. Design-system fixes from a cleanup pass over the surfaces this branch touched: - Restore accessible names lost when the whitelabeling Upload buttons were removed. The thumbnail is now the only click target, and it contained just an icon, so it announced as an unlabeled button; the icon-only Remove had the same problem. Both now carry aria-labels reflecting their state. - Use Chip, not the legacy Button, for the domain actions — Button is ~26px against the 30px ChipInput beside it, so "Add domain" sat visibly short. - SettingRow now uses the emcn Info component; a bare svg as Tooltip.Trigger was neither focusable nor nameable. It also stops re-specifying Label's own default styling. - Use the new SettingRow error prop for the group name instead of a hand-rolled error paragraph, which is what the prop was added for. - Hoist the block-category lookup out of a sort comparator, size-* over h/w, name the staleTime constants the rules require, and import RowActionsMenu from its barrel. --- .../settings/[section]/search-params.ts | 51 ++++++++ .../components/access-control.tsx | 33 ++++- .../components/group-detail.tsx | 113 +++++++++++------- .../access-control/hooks/permission-groups.ts | 11 +- apps/sim/ee/components/setting-row.tsx | 16 +-- .../components/verified-domains-section.tsx | 16 ++- .../components/whitelabeling-settings.tsx | 10 +- apps/sim/ee/whitelabeling/hooks/whitelabel.ts | 4 +- 8 files changed, 184 insertions(+), 70 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts index 94aae591694..10d4b15793f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts @@ -80,6 +80,57 @@ export const groupIdUrlKeys = { clearOnDefault: true, } as const +/** + * `group-tab` is the active tab inside the deep-linked permission-group detail + * view, so a shared `group-id` link can land on the same tab (mirrors + * `server-tab` on the workflow MCP server detail). + */ +export const groupTabParam = { + key: 'group-tab', + parser: parseAsStringLiteral(['general', 'providers', 'blocks', 'platform'] as const).withDefault( + 'general' + ), +} as const + +/** Tab view-state: clean URLs, no back-stack churn. */ +export const groupTabUrlKeys = { + history: 'replace', + clearOnDefault: true, +} as const + +/** + * `group-search` is the search box inside the permission-group detail view. The + * provider/block/platform tabs never render together, so they share one param + * rather than carrying three mutually-exclusive keys; the tab handler clears it + * so a query cannot bleed across tabs. Distinct from the list's shared + * `?search=` (`useSettingsSearch`), which belongs to the group list behind it. + */ +export const groupSearchParam = { + key: 'group-search', + parser: parseAsString.withDefault(''), +} as const + +/** Search view-state: clean URLs, no back-stack churn. */ +export const groupSearchUrlKeys = { + history: 'replace', + clearOnDefault: true, +} as const + +/** + * `group-status` filters the permission-group detail's toggle lists by enabled + * state. Shared across the tabs for the same reason as `group-search`. + */ +export const groupStatusParam = { + key: 'group-status', + parser: parseAsStringLiteral(['all', 'enabled', 'disabled'] as const).withDefault('all'), +} as const + +/** Filter view-state: clean URLs, no back-stack churn. */ +export const groupStatusUrlKeys = { + history: 'replace', + clearOnDefault: true, +} as const + /** * `custom-block-id` deep-links the Custom Blocks settings tab to a specific * block's detail sub-view. The "create new" flow stays in local state — only diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 813b2d9df80..0de43b75cf5 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -22,6 +22,12 @@ import { getEnv, isTruthy } from '@/lib/core/config/env' import { groupIdParam, groupIdUrlKeys, + groupSearchParam, + groupSearchUrlKeys, + groupStatusParam, + groupStatusUrlKeys, + groupTabParam, + groupTabUrlKeys, } from '@/app/workspace/[workspaceId]/settings/[section]/search-params' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' @@ -85,6 +91,29 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon ...groupIdParam.parser, ...groupIdUrlKeys, }) + + // Params scoped to the detail sub-view are cleared alongside the group id, so + // a tab/search/filter can't linger on the list URL after going back. nuqs + // batches these same-tick writes into a single URL update. + const [, setGroupTab] = useQueryState(groupTabParam.key, { + ...groupTabParam.parser, + ...groupTabUrlKeys, + }) + const [, setGroupSearch] = useQueryState(groupSearchParam.key, { + ...groupSearchParam.parser, + ...groupSearchUrlKeys, + }) + const [, setGroupStatus] = useQueryState(groupStatusParam.key, { + ...groupStatusParam.parser, + ...groupStatusUrlKeys, + }) + + const closeGroupDetail = useCallback(() => { + void setSelectedGroupId(null, { history: 'replace' }) + void setGroupTab(null) + void setGroupSearch(null) + void setGroupStatus(null) + }, [setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus]) const [showCreateModal, setShowCreateModal] = useState(false) const [newGroupName, setNewGroupName] = useState('') const [newGroupDescription, setNewGroupDescription] = useState('') @@ -169,8 +198,8 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon workspaceOptions={workspaceOptions} organizationWorkspaces={organizationWorkspaces} workspacesLoading={workspacesLoading} - onBack={() => void setSelectedGroupId(null, { history: 'replace' })} - onDeleted={() => void setSelectedGroupId(null, { history: 'replace' })} + onBack={closeGroupDetail} + onDeleted={closeGroupDetail} /> ) } diff --git a/apps/sim/ee/access-control/components/group-detail.tsx b/apps/sim/ee/access-control/components/group-detail.tsx index 1b70ad89a26..6b923d33c84 100644 --- a/apps/sim/ee/access-control/components/group-detail.tsx +++ b/apps/sim/ee/access-control/components/group-detail.tsx @@ -27,10 +27,19 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { formatDate } from '@sim/utils/formatting' import { ChevronDown, Plus } from 'lucide-react' +import { useQueryState } from 'nuqs' import type { ShareAuthType } from '@/lib/api/contracts/public-shares' import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail' +import { + groupSearchParam, + groupSearchUrlKeys, + groupStatusParam, + groupStatusUrlKeys, + groupTabParam, + groupTabUrlKeys, +} from '@/app/workspace/[workspaceId]/settings/[section]/search-params' import { MemberAvatar, MemberRow, @@ -58,6 +67,7 @@ import { SettingRow } from '@/ee/components/setting-row' import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' import { useOrganizationRoster } from '@/hooks/queries/organization' import { useProviderModels } from '@/hooks/queries/providers' +import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter' import { DYNAMIC_MODEL_PROVIDERS, getProviderModels, @@ -72,6 +82,9 @@ const logger = createLogger('AccessControlGroupDetail') type ConfigTab = 'general' | 'providers' | 'blocks' | 'platform' +/** Hoisted: rebuilding this per comparison allocated once per sort step. */ +const BLOCK_CATEGORY_ORDER: Record = { triggers: 0, blocks: 1, tools: 2 } + /** Public-file-share auth modes an admin can allow/disallow. `null` config = all allowed. */ const FILE_SHARE_AUTH_TYPE_OPTIONS: { value: ShareAuthType; label: string }[] = [ { value: 'public', label: 'Anyone with link' }, @@ -651,7 +664,7 @@ function ProviderRow({ onCheckedChange={() => onToggleProvider()} />
- {ProviderIcon && } + {ProviderIcon && }
+
)} @@ -144,9 +144,13 @@ export function VerifiedDomainsSection({ organizationId }: VerifiedDomainsSectio placeholder='acme.com' className='min-w-0 flex-1' /> - +
diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index 4f1021622a8..cc5752d51db 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -336,7 +336,9 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe type='button' onClick={logoUpload.handleThumbnailClick} disabled={logoUpload.isUploading} - className='group relative flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50' + aria-label={logoUpload.previewUrl ? 'Change logo' : 'Upload logo'} + title={logoUpload.previewUrl ? 'Change logo' : 'Upload logo'} + className='group relative flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50' > {logoUpload.isUploading ? ( @@ -358,6 +360,7 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe variant='ghost' size='sm' onClick={logoUpload.handleRemove} + aria-label='Remove logo' className='text-[var(--text-muted)] text-small hover:text-[var(--text-primary)]' > @@ -383,7 +386,9 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe type='button' onClick={wordmarkUpload.handleThumbnailClick} disabled={wordmarkUpload.isUploading} - className='group relative flex h-16 w-full items-center justify-center overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50' + aria-label={wordmarkUpload.previewUrl ? 'Change wordmark' : 'Upload wordmark'} + title={wordmarkUpload.previewUrl ? 'Change wordmark' : 'Upload wordmark'} + className='group relative flex h-16 w-full items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50' > {wordmarkUpload.isUploading ? ( @@ -405,6 +410,7 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe variant='ghost' size='sm' onClick={wordmarkUpload.handleRemove} + aria-label='Remove wordmark' className='text-[var(--text-muted)] text-small hover:text-[var(--text-primary)]' > diff --git a/apps/sim/ee/whitelabeling/hooks/whitelabel.ts b/apps/sim/ee/whitelabeling/hooks/whitelabel.ts index 4cd0a5895cc..f005d87f8d5 100644 --- a/apps/sim/ee/whitelabeling/hooks/whitelabel.ts +++ b/apps/sim/ee/whitelabeling/hooks/whitelabel.ts @@ -9,6 +9,8 @@ import { import type { OrganizationWhitelabelSettings } from '@/lib/branding/types' import { organizationKeys } from '@/hooks/queries/organization' +export const WHITELABEL_STALE_TIME = 60 * 1000 + /** PUT payload — string fields accept null to clear a previously-set value. */ export type WhitelabelSettingsPayload = { [K in keyof OrganizationWhitelabelSettings]: OrganizationWhitelabelSettings[K] extends @@ -46,7 +48,7 @@ export function useWhitelabelSettings(orgId: string | undefined) { queryKey: whitelabelKeys.settings(orgId ?? ''), queryFn: ({ signal }) => fetchWhitelabelSettings(orgId as string, signal), enabled: Boolean(orgId), - staleTime: 60 * 1000, + staleTime: WHITELABEL_STALE_TIME, }) } From a606b95eccb57b5c83e51b4a55ba8a464d0f6891 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 24 Jul 2026 18:53:16 -0700 Subject: [PATCH 3/5] fix(settings): alias the old /settings/domains path to SSO Folding verified domains into the SSO page dropped /settings/domains, so bookmarks and shared links 404'd instead of landing where domains now live. Both alias maps already exist for exactly this (organization/'members', subscription/'billing'); add domains -> sso to each. --- .../sim/app/workspace/[workspaceId]/settings/[section]/page.tsx | 2 ++ apps/sim/components/settings/navigation.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index 924ecc95aba..6303933ab1c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -36,6 +36,8 @@ const SECTION_ALIASES: Readonly> = { subscription: 'billing', team: 'organization', 'api-keys': 'apikeys', + // Verified domains moved into the SSO page; keep old links working. + domains: 'sso', } const TOP_LEVEL_REDIRECTS: Readonly string>> = { diff --git a/apps/sim/components/settings/navigation.ts b/apps/sim/components/settings/navigation.ts index acf5ac6c8b6..24721ece3fc 100644 --- a/apps/sim/components/settings/navigation.ts +++ b/apps/sim/components/settings/navigation.ts @@ -220,6 +220,8 @@ export const ACCOUNT_SETTINGS_PATH_ALIASES = { export const ORGANIZATION_SETTINGS_PATH_ALIASES = { organization: 'members', + // Verified domains moved into the SSO page; keep old links working. + domains: 'sso', } as const satisfies Readonly> export const WORKSPACE_SETTINGS_PATH_ALIASES = { From 02aa9a1ac64e90af9626340ca0491173429a1f3c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 24 Jul 2026 19:05:49 -0700 Subject: [PATCH 4/5] chore(settings): adopt ChipCopyInput, named staleTime constants, and a11y labels --- .../components/group-detail.tsx | 16 ++++-- .../access-control/hooks/permission-groups.ts | 12 ++--- .../ee/audit-logs/components/audit-logs.tsx | 7 ++- apps/sim/ee/audit-logs/hooks/audit-logs.ts | 4 +- apps/sim/ee/components/setting-row.tsx | 15 +++++- .../components/custom-block-detail.tsx | 7 +-- apps/sim/ee/data-drains/hooks/data-drains.ts | 7 ++- .../ee/data-retention/hooks/data-retention.ts | 4 +- .../ee/sso/components/sso-settings.test.tsx | 1 + apps/sim/ee/sso/components/sso-settings.tsx | 52 ++----------------- .../components/verified-domains-section.tsx | 1 + apps/sim/ee/sso/hooks/sso.ts | 4 +- .../components/whitelabeling-settings.tsx | 2 +- .../ee/workspace-forking/components/forks.tsx | 2 +- .../hooks/use-forking-available.ts | 2 +- 15 files changed, 65 insertions(+), 71 deletions(-) diff --git a/apps/sim/ee/access-control/components/group-detail.tsx b/apps/sim/ee/access-control/components/group-detail.tsx index 6b923d33c84..1c1f19b92c8 100644 --- a/apps/sim/ee/access-control/components/group-detail.tsx +++ b/apps/sim/ee/access-control/components/group-detail.tsx @@ -1705,7 +1705,10 @@ export function GroupDetail({ onChange={(e) => setSearchTerm(e.target.value)} className='min-w-0 flex-1' /> - + void setStatusFilter(next)} + /> setProvidersAllowed(filteredProviders, !filteredProvidersAllAllowed)} @@ -1748,7 +1751,11 @@ export function GroupDetail({ onChange={(e) => setSearchTerm(e.target.value)} className='min-w-0 flex-1' /> - + void setStatusFilter(next)} + flush + />
{filteredCoreBlocks.length === 0 && filteredToolBlocks.length === 0 && ( @@ -1851,7 +1858,10 @@ export function GroupDetail({ onChange={(e) => setSearchTerm(e.target.value)} className='min-w-0 flex-1' /> - + void setStatusFilter(next)} + /> setEditingConfig((prev) => ({ diff --git a/apps/sim/ee/access-control/hooks/permission-groups.ts b/apps/sim/ee/access-control/hooks/permission-groups.ts index ef22e847664..0f1b7f68ce9 100644 --- a/apps/sim/ee/access-control/hooks/permission-groups.ts +++ b/apps/sim/ee/access-control/hooks/permission-groups.ts @@ -19,8 +19,8 @@ import { } from '@/lib/api/contracts' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' -export const PERMISSION_GROUPS_STALE_TIME_30S = 30 * 1000 -export const PERMISSION_GROUPS_STALE_TIME_60S = 60 * 1000 +export const PERMISSION_GROUP_MEMBERS_STALE_TIME = 30 * 1000 +export const PERMISSION_GROUPS_STALE_TIME = 60 * 1000 export type { PermissionGroup, @@ -57,7 +57,7 @@ export function usePermissionGroups(organizationId?: string, enabled = true) { return data.permissionGroups ?? [] }, enabled: Boolean(organizationId) && enabled, - staleTime: PERMISSION_GROUPS_STALE_TIME_60S, + staleTime: PERMISSION_GROUPS_STALE_TIME, }) } @@ -73,7 +73,7 @@ export function usePermissionGroupMembers(organizationId?: string, permissionGro return data.members ?? [] }, enabled: Boolean(organizationId) && Boolean(permissionGroupId), - staleTime: PERMISSION_GROUPS_STALE_TIME_30S, + staleTime: PERMISSION_GROUP_MEMBERS_STALE_TIME, }) } @@ -89,7 +89,7 @@ export function useOrganizationWorkspaces(organizationId?: string, enabled = tru return data.workspaces }, enabled: Boolean(organizationId) && enabled, - staleTime: PERMISSION_GROUPS_STALE_TIME_60S, + staleTime: PERMISSION_GROUPS_STALE_TIME, }) } @@ -104,7 +104,7 @@ export function useUserPermissionConfig(workspaceId?: string) { return data }, enabled: Boolean(workspaceId), - staleTime: PERMISSION_GROUPS_STALE_TIME_60S, + staleTime: PERMISSION_GROUPS_STALE_TIME, }) } diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index 472e20f95a7..bb6fa438ecf 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -450,7 +450,12 @@ export function AuditLogs({ organizationId }: AuditLogsProps) { - diff --git a/apps/sim/ee/audit-logs/hooks/audit-logs.ts b/apps/sim/ee/audit-logs/hooks/audit-logs.ts index 04f5c08bdb6..7685f83b3ff 100644 --- a/apps/sim/ee/audit-logs/hooks/audit-logs.ts +++ b/apps/sim/ee/audit-logs/hooks/audit-logs.ts @@ -2,6 +2,8 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { requestJson } from '@/lib/api/client/request' import { type AuditLogPage, listAuditLogsContract } from '@/lib/api/contracts/audit-logs' +export const AUDIT_LOG_LIST_STALE_TIME = 30 * 1000 + export const auditLogKeys = { all: ['audit-logs'] as const, lists: () => [...auditLogKeys.all, 'list'] as const, @@ -47,6 +49,6 @@ export function useAuditLogs(organizationId: string, filters: AuditLogFilters, e initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.nextCursor, enabled: Boolean(organizationId) && enabled, - staleTime: 30 * 1000, + staleTime: AUDIT_LOG_LIST_STALE_TIME, }) } diff --git a/apps/sim/ee/components/setting-row.tsx b/apps/sim/ee/components/setting-row.tsx index f8d72cc0509..2b6eb0826f6 100644 --- a/apps/sim/ee/components/setting-row.tsx +++ b/apps/sim/ee/components/setting-row.tsx @@ -9,6 +9,12 @@ interface SettingRowProps { optional?: boolean /** Validation message rendered beneath the control. */ error?: React.ReactNode + /** + * Id of the control this row labels. Wires the label to the control so + * clicking it focuses the field, and points the control at the error text via + * `aria-describedby` — pass the same id to the child input. + */ + htmlFor?: string children: React.ReactNode } @@ -18,12 +24,13 @@ export function SettingRow({ labelTooltip, optional = false, error, + htmlFor, children, }: SettingRowProps) { return (
-
{description &&

{description}

} {children} - {error ?

{error}

: null} + {error ? ( +

+ {error} +

+ ) : null}
) } diff --git a/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx index f60a85b95ce..d3a591c0841 100644 --- a/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx +++ b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx @@ -224,7 +224,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD [deployedLoaded, availableFields, overrideById, inputs] ) - const [expandedInputs, setExpandedInputs] = useState>(new Set()) + const [expandedInputs, setExpandedInputs] = useState>(() => new Set()) const toggleInput = (id: string) => setExpandedInputs((prev) => { const next = new Set(prev) @@ -537,7 +537,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD size='sm' onClick={iconUpload.handleThumbnailClick} disabled={iconUpload.isUploading || !canManageBlock} - className='text-[13px]' + className='text-small' > {iconUrl ? 'Change' : 'Upload'} @@ -547,8 +547,9 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD variant='ghost' size='sm' onClick={iconUpload.handleRemove} + aria-label='Remove icon' disabled={iconUpload.isUploading || !canManageBlock} - className='text-[13px] text-[var(--text-muted)] hover:text-[var(--text-primary)]' + className='text-[var(--text-muted)] text-small hover:text-[var(--text-primary)]' > diff --git a/apps/sim/ee/data-drains/hooks/data-drains.ts b/apps/sim/ee/data-drains/hooks/data-drains.ts index 5a1bed9c966..5526a57aaac 100644 --- a/apps/sim/ee/data-drains/hooks/data-drains.ts +++ b/apps/sim/ee/data-drains/hooks/data-drains.ts @@ -15,6 +15,9 @@ import { updateDataDrainContract, } from '@/lib/api/contracts/data-drains' +export const DATA_DRAIN_LIST_STALE_TIME = 60 * 1000 +export const DATA_DRAIN_RUNS_STALE_TIME = 30 * 1000 + const logger = createLogger('DataDrainsQueries') export const dataDrainKeys = { @@ -54,7 +57,7 @@ export function useDataDrains(organizationId?: string) { queryKey: dataDrainKeys.list(organizationId), queryFn: ({ signal }) => fetchDataDrains(organizationId as string, signal), enabled: Boolean(organizationId), - staleTime: 60 * 1000, + staleTime: DATA_DRAIN_LIST_STALE_TIME, }) } @@ -64,7 +67,7 @@ export function useDataDrainRuns(organizationId?: string, drainId?: string, limi queryFn: ({ signal }) => fetchDataDrainRuns(organizationId as string, drainId as string, limit, signal), enabled: Boolean(organizationId && drainId), - staleTime: 30 * 1000, + staleTime: DATA_DRAIN_RUNS_STALE_TIME, placeholderData: keepPreviousData, }) } diff --git a/apps/sim/ee/data-retention/hooks/data-retention.ts b/apps/sim/ee/data-retention/hooks/data-retention.ts index f1e85a890a2..04219abeccc 100644 --- a/apps/sim/ee/data-retention/hooks/data-retention.ts +++ b/apps/sim/ee/data-retention/hooks/data-retention.ts @@ -10,6 +10,8 @@ import { updateOrganizationDataRetentionContract, } from '@/lib/api/contracts/organization' +export const DATA_RETENTION_STALE_TIME = 60 * 1000 + export type RetentionValues = OrganizationRetentionValues export type DataRetentionResponse = OrganizationDataRetention @@ -34,7 +36,7 @@ export function useOrganizationRetention(orgId: string | undefined) { queryKey: dataRetentionKeys.settings(orgId ?? ''), queryFn: ({ signal }) => fetchDataRetention(orgId as string, signal), enabled: Boolean(orgId), - staleTime: 60 * 1000, + staleTime: DATA_RETENTION_STALE_TIME, }) } diff --git a/apps/sim/ee/sso/components/sso-settings.test.tsx b/apps/sim/ee/sso/components/sso-settings.test.tsx index 0f584bba706..2a400aaf8f0 100644 --- a/apps/sim/ee/sso/components/sso-settings.test.tsx +++ b/apps/sim/ee/sso/components/sso-settings.test.tsx @@ -21,6 +21,7 @@ vi.mock('@sim/emcn', () => ({ ), ChipCombobox: () =>
, + ChipCopyInput: ({ value }: { value?: string }) => , ChipInput: ({ value, onChange, diff --git a/apps/sim/ee/sso/components/sso-settings.tsx b/apps/sim/ee/sso/components/sso-settings.tsx index 6ac9233cdb5..07c18650880 100644 --- a/apps/sim/ee/sso/components/sso-settings.tsx +++ b/apps/sim/ee/sso/components/sso-settings.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { Button, ChipCombobox, + ChipCopyInput, ChipInput, ChipSelect, ChipTextarea, @@ -15,7 +16,7 @@ import { } from '@sim/emcn' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { Check, ChevronDown, Clipboard, Eye, EyeOff } from 'lucide-react' +import { ChevronDown, Eye, EyeOff } from 'lucide-react' import type { SsoRegistrationBody } from '@/lib/api/contracts/auth' import { useSession } from '@/lib/auth/auth-client' import { isEnterprise } from '@/lib/billing/plan-helpers' @@ -106,7 +107,6 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { const configureSSOMutation = useConfigureSSO() const [showClientSecret, setShowClientSecret] = useState(false) - const [copied, setCopied] = useState(false) const [isEditing, setIsEditing] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) @@ -330,14 +330,6 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { const isSaml = formData.providerType === 'saml' const callbackUrl = `${getBaseUrl()}/api/auth/${isSaml ? 'sso/saml2/callback' : 'sso/callback'}/${formData.providerId || existingProvider?.providerId || 'provider-id'}` - const copyToClipboard = async (url: string) => { - try { - await navigator.clipboard.writeText(url) - setCopied(true) - setTimeout(() => setCopied(false), 1500) - } catch {} - } - const handleEdit = () => { if (!existingProvider) return @@ -423,25 +415,7 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { - copyToClipboard(providerCallbackUrl)} - className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' - aria-label='Copy callback URL' - > - {copied ? ( - - ) : ( - - )} - - } - /> +

Configure this in your identity provider

@@ -792,25 +766,7 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) { )} - copyToClipboard(callbackUrl)} - className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]' - aria-label='Copy callback URL' - > - {copied ? ( - - ) : ( - - )} - - } - /> +

Configure this in your identity provider

diff --git a/apps/sim/ee/sso/components/verified-domains-section.tsx b/apps/sim/ee/sso/components/verified-domains-section.tsx index d96f2240362..e069fd16d53 100644 --- a/apps/sim/ee/sso/components/verified-domains-section.tsx +++ b/apps/sim/ee/sso/components/verified-domains-section.tsx @@ -59,6 +59,7 @@ function DomainRow({ organizationId, domain, onRemove }: DomainRowProps) { } /> + {/* pl-[46px] indents past SettingsResourceRow's icon gutter (size-9 tile + gap-2.5). */} {!isVerified && domain.txtRecordValue && (
fetchSSOProviders(signal, organizationId), - staleTime: 5 * 60 * 1000, + staleTime: SSO_PROVIDERS_STALE_TIME, enabled, }) } diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index cc5752d51db..deecd10d1ce 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -79,7 +79,7 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn return (
- +
setSelectedForkId(null, { history: 'replace' })} + onBack={() => void setSelectedForkId(null, { history: 'replace' })} actions={parentHeaderActions} /> ) : forkView === 'activity' ? ( diff --git a/apps/sim/ee/workspace-forking/hooks/use-forking-available.ts b/apps/sim/ee/workspace-forking/hooks/use-forking-available.ts index 90380a9bff7..8d9284c9533 100644 --- a/apps/sim/ee/workspace-forking/hooks/use-forking-available.ts +++ b/apps/sim/ee/workspace-forking/hooks/use-forking-available.ts @@ -9,7 +9,7 @@ export const forkAvailabilityKeys = { } /** Availability flips only on plan changes or flag rollouts - cache generously. */ -const FORK_AVAILABILITY_STALE_TIME = 5 * 60 * 1000 +export const FORK_AVAILABILITY_STALE_TIME = 5 * 60 * 1000 interface ForkingAvailability { available: boolean From 4a38eeef2191916117608920fe37f1dbba877f00 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 24 Jul 2026 19:07:36 -0700 Subject: [PATCH 5/5] fix(settings): reset group detail params on open and drop issuer mono styling --- .../components/access-control.tsx | 18 +++++++++++++++++- apps/sim/ee/sso/components/sso-settings.tsx | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 0de43b75cf5..6d2cb78d2e3 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -108,6 +108,22 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon ...groupStatusUrlKeys, }) + /** + * The detail view's tab/search/status params are scoped to one group, so both + * transitions reset them — otherwise a stale `group-id` that never resolves + * leaves them in the URL and the next group opens on the previous group's tab + * and filters. nuqs batches these same-tick writes into one URL update. + */ + const openGroupDetail = useCallback( + (groupId: string) => { + void setSelectedGroupId(groupId) + void setGroupTab(null) + void setGroupSearch(null) + void setGroupStatus(null) + }, + [setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus] + ) + const closeGroupDetail = useCallback(() => { void setSelectedGroupId(null, { history: 'replace' }) void setGroupTab(null) @@ -236,7 +252,7 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon