Skip to content

Commit 919a98d

Browse files
authored
refactor(settings): fold verified domains into SSO, move group-detail state to nuqs, design-system cleanup (#5950)
* refactor(settings): fold verified domains into SSO and use shared primitives 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. * fix(settings): move group-detail view state to nuqs and clean up design-system drift 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. * 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. * chore(settings): adopt ChipCopyInput, named staleTime constants, and a11y labels * fix(settings): reset group detail params on open and drop issuer mono styling
1 parent f43b52c commit 919a98d

26 files changed

Lines changed: 687 additions & 616 deletions

File tree

apps/docs/content/docs/en/platform/enterprise/verified-domains.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Verified Domains let organization owners and admins on Enterprise plans prove th
1616

1717
## Verify a domain
1818

19-
Go to **Settings → Security → Verified domains** in your organization settings.
19+
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.
2020

2121
1. Enter the domain, for example `acme.com`, and click **Add domain**.
2222
2. Sim shows a DNS **TXT record** to publish — a host (`_sim-challenge.acme.com`) and a unique value (`sim-domain-verification=…`).

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
148148
const domainNotVerifiedResponse = () =>
149149
NextResponse.json(
150150
{
151-
error: `Verify ownership of ${domain} under Settings → Verified domains before configuring SSO for it.`,
151+
error: `Verify ownership of ${domain} under Verified domains above before configuring SSO for it.`,
152152
code: 'SSO_DOMAIN_NOT_VERIFIED',
153153
},
154154
{ status: 403 }

apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const SECTION_ALIASES: Readonly<Record<string, SettingsSection>> = {
3636
subscription: 'billing',
3737
team: 'organization',
3838
'api-keys': 'apikeys',
39+
// Verified domains moved into the SSO page; keep old links working.
40+
domains: 'sso',
3941
}
4042

4143
const TOP_LEVEL_REDIRECTS: Readonly<Record<string, (workspaceId: string) => string>> = {

apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,57 @@ export const groupIdUrlKeys = {
8080
clearOnDefault: true,
8181
} as const
8282

83+
/**
84+
* `group-tab` is the active tab inside the deep-linked permission-group detail
85+
* view, so a shared `group-id` link can land on the same tab (mirrors
86+
* `server-tab` on the workflow MCP server detail).
87+
*/
88+
export const groupTabParam = {
89+
key: 'group-tab',
90+
parser: parseAsStringLiteral(['general', 'providers', 'blocks', 'platform'] as const).withDefault(
91+
'general'
92+
),
93+
} as const
94+
95+
/** Tab view-state: clean URLs, no back-stack churn. */
96+
export const groupTabUrlKeys = {
97+
history: 'replace',
98+
clearOnDefault: true,
99+
} as const
100+
101+
/**
102+
* `group-search` is the search box inside the permission-group detail view. The
103+
* provider/block/platform tabs never render together, so they share one param
104+
* rather than carrying three mutually-exclusive keys; the tab handler clears it
105+
* so a query cannot bleed across tabs. Distinct from the list's shared
106+
* `?search=` (`useSettingsSearch`), which belongs to the group list behind it.
107+
*/
108+
export const groupSearchParam = {
109+
key: 'group-search',
110+
parser: parseAsString.withDefault(''),
111+
} as const
112+
113+
/** Search view-state: clean URLs, no back-stack churn. */
114+
export const groupSearchUrlKeys = {
115+
history: 'replace',
116+
clearOnDefault: true,
117+
} as const
118+
119+
/**
120+
* `group-status` filters the permission-group detail's toggle lists by enabled
121+
* state. Shared across the tabs for the same reason as `group-search`.
122+
*/
123+
export const groupStatusParam = {
124+
key: 'group-status',
125+
parser: parseAsStringLiteral(['all', 'enabled', 'disabled'] as const).withDefault('all'),
126+
} as const
127+
128+
/** Filter view-state: clean URLs, no back-stack churn. */
129+
export const groupStatusUrlKeys = {
130+
history: 'replace',
131+
clearOnDefault: true,
132+
} as const
133+
83134
/**
84135
* `custom-block-id` deep-links the Custom Blocks settings tab to a specific
85136
* block's detail sub-view. The "create new" flow stays in local state — only

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,6 @@ const AuditLogs = dynamic(() =>
8181
import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs)
8282
)
8383
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO))
84-
const DomainSettings = dynamic(() =>
85-
import('@/ee/sso/components/domain-settings').then((m) => m.DomainSettings)
86-
)
8784
const SessionPolicySettings = dynamic(() =>
8885
import('@/ee/session-policy/components/session-policy-settings').then(
8986
(m) => m.SessionPolicySettings
@@ -166,9 +163,6 @@ export function SettingsPage({ section }: SettingsPageProps) {
166163
/>
167164
)}
168165
{effectiveSection === 'sso' && organizationId && <SSO organizationId={organizationId} />}
169-
{effectiveSection === 'domains' && organizationId && (
170-
<DomainSettings key={organizationId} organizationId={organizationId} />
171-
)}
172166
{effectiveSection === 'sessions' && organizationId && (
173167
<SessionPolicySettings key={organizationId} organizationId={organizationId} />
174168
)}

apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ describe('unified settings navigation', () => {
3939
{ id: 'inbox', label: 'Sim mailer', section: 'system' },
4040
{ id: 'recently-deleted', label: 'Recently deleted', section: 'system' },
4141
{ id: 'sso', label: 'Single sign-on', section: 'enterprise' },
42-
{ id: 'domains', label: 'Verified domains', section: 'enterprise' },
4342
{ id: 'sessions', label: 'Session policies', section: 'enterprise' },
4443
{ id: 'data-retention', label: 'Data retention', section: 'enterprise' },
4544
{ id: 'data-drains', label: 'Data drains', section: 'enterprise' },

apps/sim/components/settings/navigation.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ describe('settings navigation boundaries', () => {
4242
'inbox',
4343
'recently-deleted',
4444
'sso',
45-
'domains',
4645
'sessions',
4746
'data-retention',
4847
'data-drains',
@@ -65,7 +64,6 @@ describe('settings navigation boundaries', () => {
6564
'access-control',
6665
'audit-logs',
6766
'sso',
68-
'domains',
6967
'sessions',
7068
'data-retention',
7169
'data-drains',
@@ -121,7 +119,6 @@ describe('settings navigation boundaries', () => {
121119
'billing',
122120
'data-drains',
123121
'data-retention',
124-
'domains',
125122
'organization',
126123
'sessions',
127124
'sso',

apps/sim/components/settings/navigation.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
HexSimple,
77
Key,
88
KeySquare,
9-
Link,
109
Lock,
1110
LogIn,
1211
Palette,
@@ -43,7 +42,6 @@ export type OrganizationSettingsSection =
4342
| 'access-control'
4443
| 'audit-logs'
4544
| 'sso'
46-
| 'domains'
4745
| 'sessions'
4846
| 'data-retention'
4947
| 'data-drains'
@@ -90,7 +88,6 @@ export type UnifiedSettingsSection =
9088
| 'teammates'
9189
| 'organization'
9290
| 'sso'
93-
| 'domains'
9491
| 'whitelabeling'
9592
| 'copilot'
9693
| 'forks'
@@ -223,6 +220,8 @@ export const ACCOUNT_SETTINGS_PATH_ALIASES = {
223220

224221
export const ORGANIZATION_SETTINGS_PATH_ALIASES = {
225222
organization: 'members',
223+
// Verified domains moved into the SSO page; keep old links working.
224+
domains: 'sso',
226225
} as const satisfies Readonly<Record<string, OrganizationSettingsSection>>
227226

228227
export const WORKSPACE_SETTINGS_PATH_ALIASES = {
@@ -544,22 +543,6 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
544543
organization: { id: 'sso', group: 'security', order: 4 },
545544
},
546545
},
547-
{
548-
label: 'Verified domains',
549-
icon: Link,
550-
docsLink: 'https://docs.sim.ai/platform/enterprise/verified-domains',
551-
unified: {
552-
id: 'domains',
553-
description: 'Prove ownership of your email domains before configuring SSO.',
554-
group: 'enterprise',
555-
requiresHosted: true,
556-
requiresEnterprise: true,
557-
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
558-
},
559-
planes: {
560-
organization: { id: 'domains', group: 'security', order: 5 },
561-
},
562-
},
563546
{
564547
label: 'Session policies',
565548
icon: Clock,
@@ -573,7 +556,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
573556
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies,
574557
},
575558
planes: {
576-
organization: { id: 'sessions', group: 'security', order: 6 },
559+
organization: { id: 'sessions', group: 'security', order: 5 },
577560
},
578561
},
579562
{
@@ -590,7 +573,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
590573
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention,
591574
},
592575
planes: {
593-
organization: { id: 'data-retention', group: 'enterprise', order: 7 },
576+
organization: { id: 'data-retention', group: 'enterprise', order: 6 },
594577
},
595578
},
596579
{
@@ -606,7 +589,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
606589
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains,
607590
},
608591
planes: {
609-
organization: { id: 'data-drains', group: 'enterprise', order: 8 },
592+
organization: { id: 'data-drains', group: 'enterprise', order: 7 },
610593
},
611594
},
612595
{
@@ -622,7 +605,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
622605
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.whitelabeling,
623606
},
624607
planes: {
625-
organization: { id: 'whitelabeling', group: 'enterprise', order: 9 },
608+
organization: { id: 'whitelabeling', group: 'enterprise', order: 8 },
626609
},
627610
},
628611
{
@@ -758,7 +741,6 @@ export function getOrganizationSettingsFeatures(
758741
'access-control': SETTINGS_SELF_HOSTED_OVERRIDES.accessControl,
759742
'audit-logs': SETTINGS_SELF_HOSTED_OVERRIDES.auditLogs,
760743
sso: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
761-
domains: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
762744
sessions: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies,
763745
'data-retention': SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention,
764746
'data-drains': SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains,

apps/sim/components/settings/organization-settings-renderer.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ const AuditLogs = dynamic(() =>
2323
import('@/ee/audit-logs/components/audit-logs').then((module) => module.AuditLogs)
2424
)
2525
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((module) => module.SSO))
26-
const DomainSettings = dynamic(() =>
27-
import('@/ee/sso/components/domain-settings').then((module) => module.DomainSettings)
28-
)
2926
const SessionPolicySettings = dynamic(() =>
3027
import('@/ee/session-policy/components/session-policy-settings').then(
3128
(module) => module.SessionPolicySettings
@@ -71,9 +68,6 @@ export function OrganizationSettingsRenderer({
7168
}
7269
if (section === 'audit-logs') return <AuditLogs organizationId={organizationId} />
7370
if (section === 'sso') return <SSO organizationId={organizationId} />
74-
if (section === 'domains') {
75-
return <DomainSettings key={organizationId} organizationId={organizationId} />
76-
}
7771
if (section === 'sessions') {
7872
return <SessionPolicySettings key={organizationId} organizationId={organizationId} />
7973
}

apps/sim/ee/access-control/components/access-control.tsx

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import { getEnv, isTruthy } from '@/lib/core/config/env'
2222
import {
2323
groupIdParam,
2424
groupIdUrlKeys,
25+
groupSearchParam,
26+
groupSearchUrlKeys,
27+
groupStatusParam,
28+
groupStatusUrlKeys,
29+
groupTabParam,
30+
groupTabUrlKeys,
2531
} from '@/app/workspace/[workspaceId]/settings/[section]/search-params'
2632
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2733
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
@@ -85,6 +91,45 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
8591
...groupIdParam.parser,
8692
...groupIdUrlKeys,
8793
})
94+
95+
// Params scoped to the detail sub-view are cleared alongside the group id, so
96+
// a tab/search/filter can't linger on the list URL after going back. nuqs
97+
// batches these same-tick writes into a single URL update.
98+
const [, setGroupTab] = useQueryState(groupTabParam.key, {
99+
...groupTabParam.parser,
100+
...groupTabUrlKeys,
101+
})
102+
const [, setGroupSearch] = useQueryState(groupSearchParam.key, {
103+
...groupSearchParam.parser,
104+
...groupSearchUrlKeys,
105+
})
106+
const [, setGroupStatus] = useQueryState(groupStatusParam.key, {
107+
...groupStatusParam.parser,
108+
...groupStatusUrlKeys,
109+
})
110+
111+
/**
112+
* The detail view's tab/search/status params are scoped to one group, so both
113+
* transitions reset them — otherwise a stale `group-id` that never resolves
114+
* leaves them in the URL and the next group opens on the previous group's tab
115+
* and filters. nuqs batches these same-tick writes into one URL update.
116+
*/
117+
const openGroupDetail = useCallback(
118+
(groupId: string) => {
119+
void setSelectedGroupId(groupId)
120+
void setGroupTab(null)
121+
void setGroupSearch(null)
122+
void setGroupStatus(null)
123+
},
124+
[setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus]
125+
)
126+
127+
const closeGroupDetail = useCallback(() => {
128+
void setSelectedGroupId(null, { history: 'replace' })
129+
void setGroupTab(null)
130+
void setGroupSearch(null)
131+
void setGroupStatus(null)
132+
}, [setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus])
88133
const [showCreateModal, setShowCreateModal] = useState(false)
89134
const [newGroupName, setNewGroupName] = useState('')
90135
const [newGroupDescription, setNewGroupDescription] = useState('')
@@ -169,8 +214,8 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
169214
workspaceOptions={workspaceOptions}
170215
organizationWorkspaces={organizationWorkspaces}
171216
workspacesLoading={workspacesLoading}
172-
onBack={() => void setSelectedGroupId(null, { history: 'replace' })}
173-
onDeleted={() => void setSelectedGroupId(null, { history: 'replace' })}
217+
onBack={closeGroupDetail}
218+
onDeleted={closeGroupDetail}
174219
/>
175220
)
176221
}
@@ -207,7 +252,7 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
207252
<button
208253
key={group.id}
209254
type='button'
210-
onClick={() => void setSelectedGroupId(group.id)}
255+
onClick={() => openGroupDetail(group.id)}
211256
className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
212257
>
213258
<div className='flex min-w-0 flex-1 flex-col'>

0 commit comments

Comments
 (0)