Skip to content

Commit 93cd7f1

Browse files
committed
Merge branch 'wip/fx33-c' into wip/platform-org-wiring
# Conflicts: # scripts/platform-allowlists.ts
2 parents 803b4f5 + a49677e commit 93cd7f1

59 files changed

Lines changed: 3286 additions & 3094 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Button } from '@sim/emcn/button'
2+
import Link from 'next/link'
3+
4+
interface ExternalLinkProps {
5+
href: string
6+
/** The link's whole name, rendered once: Docs, Managed keys, Open in Slack. */
7+
label: string
8+
}
9+
10+
/**
11+
* The one way an organization Settings page links out: a quiet compact button that opens `href` in a
12+
* new tab, its label rendered once and the external-link glyph trailing. A page-level link (a guide)
13+
* sits in the control bar under the page header; a record's link sits with that record.
14+
*/
15+
export function ExternalLink({ href, label }: ExternalLinkProps) {
16+
return (
17+
<Button
18+
variant='quiet'
19+
size='compact'
20+
trailingIcon='I037'
21+
render={<Link href={href} target='_blank' rel='noopener noreferrer' />}
22+
>
23+
{label}
24+
</Button>
25+
)
26+
}

‎apps/sim/app/(product)/o/[organizationId]/settings/_components/sections/access-control/access-control-section.tsx‎

Lines changed: 78 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,44 @@ import { useState } from 'react'
44
import { ActivityRow } from '@sim/emcn/activity-row'
55
import { Button } from '@sim/emcn/button'
66
import { Checkbox } from '@sim/emcn/checkbox'
7-
import { EmptyState } from '@sim/emcn/empty-state'
87
import { TextField } from '@sim/emcn/field'
98
import { Modal } from '@sim/emcn/modal'
109
import { MultiSelect } from '@sim/emcn/picker/multi-select'
1110
import { SearchField } from '@sim/emcn/search-field'
12-
import { SettingsRow, SettingsSection } from '@sim/emcn/settings-section'
13-
import { FailureNotice } from '@sim/emcn/status'
14-
import { getErrorMessage } from '@sim/utils/errors'
15-
import Link from 'next/link'
11+
import { SettingsRow } from '@sim/emcn/settings-section'
12+
import { SettingsList } from '@/app/(product)/_components/settings-parts/settings-list'
1613
import { SettingsPageHeader } from '@/app/(product)/_components/settings-parts/settings-page'
1714
import {
1815
ResourceList,
1916
SectionStack,
17+
SectionToolbar,
2018
} from '@/app/(product)/_components/settings-parts/settings-parts'
19+
import { SettingsRead } from '@/app/(product)/_components/settings-parts/settings-read'
2120
import type { SettingsSectionProps } from '@/app/(product)/o/[organizationId]/settings/_components/section-registry'
22-
import { GroupDetail } from '@/app/(product)/o/[organizationId]/settings/_components/sections/access-control/group-detail'
2321
import {
24-
type PermissionGroup,
22+
GroupDetail,
23+
groupSummary,
24+
} from '@/app/(product)/o/[organizationId]/settings/_components/sections/access-control/group-detail'
25+
import {
2526
useAccessControlParams,
2627
useCreatePermissionGroup,
2728
useOrganizationWorkspaces,
2829
usePermissionGroups,
2930
} from '@/platform/lib/data/access-control'
30-
import { surfaceHref } from '@/platform/lib/data/organization-model'
31-
import { usePlatformHref } from '@/platform/lib/platform-href'
32-
import { plural } from '@/platform/shared/plural'
33-
34-
function groupSummary(group: PermissionGroup): string {
35-
if (group.isDefault) return 'Everyone in the organization'
36-
const members = group.memberCount === 0 ? 'All members' : plural(group.memberCount, 'member')
37-
return `${members} · ${plural(group.workspaces.length, 'workspace')}`
38-
}
3931

4032
interface CreateGroupModalProps {
4133
organizationId: string
4234
open: boolean
4335
onOpenChange: (open: boolean) => void
44-
workspaceOptions: { value: string; label: string }[]
45-
workspacesLoading: boolean
4636
}
4737

48-
function CreateGroupModal({
49-
organizationId,
50-
open,
51-
onOpenChange,
52-
workspaceOptions,
53-
workspacesLoading,
54-
}: CreateGroupModalProps) {
38+
function CreateGroupModal({ organizationId, open, onOpenChange }: CreateGroupModalProps) {
5539
const createGroup = useCreatePermissionGroup()
40+
const workspaces = useOrganizationWorkspaces(organizationId, open)
41+
const workspaceOptions = (workspaces.data ?? []).map((workspace) => ({
42+
value: workspace.id,
43+
label: workspace.name,
44+
}))
5645
const [name, setName] = useState('')
5746
const [description, setDescription] = useState('')
5847
const [isDefault, setIsDefault] = useState(false)
@@ -73,7 +62,7 @@ function CreateGroupModal({
7362
open={open}
7463
onOpenChange={change}
7564
title='Create permission group'
76-
description='Give the group a name and choose the workspaces it governs.'
65+
description='Give the group a name and choose the workspaces it governs'
7766
confirmLabel={createGroup.isPending ? 'Creating…' : 'Create group'}
7867
confirmDisabled={!name.trim() || (!isDefault && workspaceIds.length === 0)}
7968
onConfirm={async () => {
@@ -109,7 +98,7 @@ function CreateGroupModal({
10998
options={workspaceOptions}
11099
value={workspaceIds}
111100
onValueChange={setWorkspaceIds}
112-
disabled={isDefault || workspacesLoading}
101+
disabled={isDefault || workspaces.data === undefined}
113102
/>
114103
</Modal>
115104
)
@@ -118,13 +107,9 @@ function CreateGroupModal({
118107
export default function AccessControlSection({ organization }: SettingsSectionProps) {
119108
const organizationId = organization.organization.id
120109
const groups = usePermissionGroups(organizationId)
121-
const workspaces = useOrganizationWorkspaces(organizationId)
122110
const params = useAccessControlParams()
123-
const hrefFor = usePlatformHref()
124111
const [creating, setCreating] = useState(false)
125112
const permissionGroups = groups.data ?? []
126-
const organizationWorkspaces = workspaces.data ?? []
127-
const workspaceOptions = organizationWorkspaces.map((ws) => ({ value: ws.id, label: ws.name }))
128113
const query = params.search.trim().toLowerCase()
129114
const filtered = query
130115
? permissionGroups.filter((group) => group.name.toLowerCase().includes(query))
@@ -133,109 +118,83 @@ export default function AccessControlSection({ organization }: SettingsSectionPr
133118
? permissionGroups.find((group) => group.id === params.groupId)
134119
: undefined
135120

136-
if (groups.isPending) return params.groupId ? null : <SettingsPageHeader />
137-
if (groups.error && groups.data === undefined) {
138-
return (
139-
<>
140-
<SettingsPageHeader />
141-
<FailureNotice
142-
message={getErrorMessage(groups.error, 'Failed to load permission groups')}
143-
retrying={groups.isFetching}
144-
onRetry={() => void groups.refetch()}
145-
/>
146-
</>
147-
)
148-
}
149-
150-
const workspacesError = workspaces.error && workspaces.data === undefined && (
151-
<FailureNotice
152-
message={getErrorMessage(workspaces.error, 'Failed to load workspaces')}
153-
retrying={workspaces.isFetching}
154-
onRetry={() => void workspaces.refetch()}
155-
/>
156-
)
157-
158-
const refreshError = groups.error && (
159-
<FailureNotice
160-
message={getErrorMessage(groups.error, 'Failed to refresh permission groups')}
161-
retrying={groups.isFetching}
162-
onRetry={() => void groups.refetch()}
163-
/>
164-
)
121+
/** A valid deep link never flashes the list before jumping to it; a dead id falls back to it. */
122+
if (params.groupId && groups.isPending) return null
165123

166124
if (selectedGroup) {
167125
return (
168-
<SectionStack>
169-
{refreshError}
170-
{workspacesError}
171-
<GroupDetail
172-
key={selectedGroup.id}
173-
group={selectedGroup}
174-
organizationId={organizationId}
175-
hosted={organization.deployment.hosted}
176-
projectsEnabled={organization.featureFlags.projects}
177-
workspaceOptions={workspaceOptions}
178-
workspacesLoading={workspaces.isPending}
179-
onBack={params.closeGroup}
180-
/>
181-
</SectionStack>
126+
<GroupDetail
127+
key={selectedGroup.id}
128+
group={selectedGroup}
129+
organizationId={organizationId}
130+
hosted={organization.deployment.hosted}
131+
projectsEnabled={organization.featureFlags.projects}
132+
onBack={params.closeGroup}
133+
/>
182134
)
183135
}
184136

137+
const createGroup = (
138+
<Button size='compact' icon='I074' onClick={() => setCreating(true)}>
139+
Create group
140+
</Button>
141+
)
142+
185143
return (
186144
<SectionStack>
187145
<SettingsPageHeader />
188-
{refreshError}
189-
{workspacesError}
190-
<SearchField
191-
label='Search permission groups'
192-
placeholder='Search permission groups…'
193-
value={params.search}
194-
onValueChange={params.setSearch}
195-
/>
196-
{permissionGroups.length === 0 ? (
197-
<EmptyState title='No permission groups yet' description='Create a group to get started.' />
198-
) : filtered.length === 0 ? (
199-
<EmptyState title='No matching groups' />
200-
) : (
201-
<SettingsSection title={`Permission groups ${permissionGroups.length}`}>
202-
<ResourceList>
203-
{filtered.map((group) => (
204-
<ActivityRow
205-
key={group.id}
206-
title={group.name}
207-
detail={groupSummary(group)}
208-
textMetadata={group.isDefault ? 'Default' : undefined}
209-
onOpen={() => params.openGroup(group.id)}
210-
/>
211-
))}
212-
</ResourceList>
213-
</SettingsSection>
146+
{permissionGroups.length > 0 && (
147+
<SectionToolbar
148+
grow={
149+
<SearchField
150+
label='Search permission groups'
151+
placeholder='Search permission groups…'
152+
value={params.search}
153+
onValueChange={params.setSearch}
154+
/>
155+
}
156+
/>
214157
)}
215-
<SettingsSection title='Manage'>
216-
<SettingsRow label='New permission group' description='Limit what a set of members can use'>
217-
<Button size='compact' icon='I074' onClick={() => setCreating(true)}>
218-
Create group
219-
</Button>
220-
</SettingsRow>
221-
<SettingsRow
222-
label='Access requests'
223-
description='Review requests to use restricted features'
224-
>
225-
<Button
226-
size='compact'
227-
render={<Link href={hrefFor(surfaceHref(organization, '/settings/requests'))} />}
158+
<SettingsRead query={groups} failureMessage='Failed to load permission groups'>
159+
{(list) => (
160+
<SettingsList
161+
title='Permission groups'
162+
empty={
163+
list.length === 0
164+
? {
165+
title: 'No permission groups yet',
166+
description: 'Limit what a set of members can use',
167+
action: createGroup,
168+
}
169+
: filtered.length === 0
170+
? { title: 'No matching groups' }
171+
: undefined
172+
}
228173
>
229-
Review requests
230-
</Button>
231-
</SettingsRow>
232-
</SettingsSection>
174+
<ResourceList>
175+
{filtered.map((group) => (
176+
<ActivityRow
177+
key={group.id}
178+
title={group.name}
179+
detail={groupSummary(group)}
180+
textMetadata={group.isDefault ? 'Default' : undefined}
181+
onOpen={() => params.openGroup(group.id)}
182+
/>
183+
))}
184+
</ResourceList>
185+
<SettingsRow
186+
label='New permission group'
187+
description='Limit what a set of members can use'
188+
>
189+
{createGroup}
190+
</SettingsRow>
191+
</SettingsList>
192+
)}
193+
</SettingsRead>
233194
<CreateGroupModal
234195
organizationId={organizationId}
235196
open={creating}
236197
onOpenChange={setCreating}
237-
workspaceOptions={workspaceOptions}
238-
workspacesLoading={workspaces.isPending}
239198
/>
240199
</SectionStack>
241200
)

0 commit comments

Comments
 (0)