Skip to content

Commit 1626e5c

Browse files
committed
refine(platform): unify modal fields and manual credentials
1 parent 72253b0 commit 1626e5c

63 files changed

Lines changed: 1232 additions & 475 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/platform/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Toolbar choices use `ChoiceMenu`, the same compact quiet button and menu as Home
3737

3838
Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges` directly to a native or Base UI scroll owner. Use the shared 16px mask only on edges with more content, updating after scroll, resize, input, reset or filtering. Platform and Settings navigation hide the native scrollbar and keep their edge fades, keyboard, wheel and touch scrolling. Other scroll areas keep their existing scrollbar behavior. Insets establish resting layout; they cannot substitute for an edge fade. Home list widgets and notification panels use `--ui-widget-max-size` as a ceiling, not a forced height. The Home activity chart grows with its content and has no nested scroll viewport. Keep the dashboard free to scroll and let each overflowing list or schedule scroll independently beneath its stationary heading. Shared ScrollArea viewports and the Home calendar suppress rubber-band overscroll at their boundaries. Plain tooltips preview at most three lines; retain the complete accessible text. A structured people popover owns its content and uses the dropdown’s popup padding and row insets.
3939

40-
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
40+
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Modal content uses the shared field family: TextField, TextAreaField, SelectField, SecretField, RecipientField, FileField, DetailsField and ListField. Product dialogs provide values, labels and callbacks; they do not recreate field chrome. Full-width FileField centers its prompt without an upload glyph; its image kind supplies a compact preview and independent removal. DetailsField labels read-only information, and ListField owns grouped bullets and bounded scrolling. Use DataTable inside a dialog only for actual tabular data or selection, not static skill instructions. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
4141

4242
ContentTabs uses Base UI tab behavior with the same ControlRow lane, spacing and paint as DestinationTabs. DataTable composes those tabs, a caller-owned toolbar, native table semantics and shared checkboxes for controlled row/select-all operations. Passive table rows have no whole-row hover or click target; their controls remain independent. Consumers own filtering, sorting and mutation policy.
4343

‎apps/sim/platform/app/(shell)/_components/resource-directory/resource-directory.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ReactNode, Ref } from 'react'
1+
import type { ComponentProps, ReactNode, Ref } from 'react'
2+
import type Link from 'next/link'
23
import styles from '@/platform/app/(shell)/_components/resource-directory/resource-directory.module.css'
34
import { Chip, PageColumn } from '@/platform/design-system/components'
45

@@ -51,11 +52,12 @@ export function DirectoryHeading({
5152
interface DirectoryBackProps {
5253
href: string
5354
label: string
55+
onNavigate?: ComponentProps<typeof Link>['onNavigate']
5456
}
55-
export function DirectoryBack({ href, label }: DirectoryBackProps) {
57+
export function DirectoryBack({ href, label, onNavigate }: DirectoryBackProps) {
5658
return (
5759
<nav aria-label={`${label} navigation`} className={styles.back}>
58-
<Chip href={href} icon='I093'>
60+
<Chip href={href} icon='I093' onNavigate={onNavigate}>
5961
{label}
6062
</Chip>
6163
</nav>

‎apps/sim/platform/app/(shell)/_components/session/integration-center-changes.tsx‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
IntegrationCenterData,
66
IntegrationResource,
77
} from '@/platform/lib/integrations/center-model'
8+
import { validCredentialMembers } from '@/platform/lib/integrations/credential-access'
9+
import { manualCredentialAccess } from '@/platform/lib/integrations/manual-secret'
810

911
function useChanges(enabled: boolean) {
1012
const [edits, setEdits] = useState<Record<string, IntegrationResource | null>>({})
@@ -45,6 +47,20 @@ function resourceMetadata(resource: IntegrationResource): IntegrationResource {
4547
kind: 'credential',
4648
key: resource.key,
4749
credentialType: resource.credentialType,
50+
appId: resource.appId,
51+
members: resource.members?.map(
52+
({ id, userId, name, email, src, role, roleSource, explicitAdmin, status }) => ({
53+
id,
54+
userId,
55+
name,
56+
email,
57+
src,
58+
role,
59+
roleSource,
60+
explicitAdmin,
61+
status,
62+
})
63+
),
4864
}
4965
}
5066
}
@@ -69,11 +85,37 @@ export function useIntegrationCenterResources(data: IntegrationCenterData | null
6985
if (!enabled || !data) return
7086
const existing = resources.find((entry) => entry.id === resource.id)
7187
if (existing && !existing.canEdit) return
88+
if (
89+
existing?.kind === 'credential' &&
90+
resource.kind === 'credential' &&
91+
resource.members &&
92+
!validCredentialMembers(manualCredentialAccess(existing, data), resource.members)
93+
)
94+
return
7295
setEdits((current) => ({ ...current, [resource.id]: resourceMetadata(resource) }))
7396
},
7497
remove(id: string) {
7598
if (!enabled || !data || !resources.find((resource) => resource.id === id)?.canEdit) return
7699
setEdits((current) => ({ ...current, [id]: null }))
77100
},
101+
leave(id: string) {
102+
if (!enabled || !data) return
103+
const resource = resources.find((resource) => resource.id === id)
104+
if (resource?.kind !== 'credential') return
105+
const credential = manualCredentialAccess(resource, data)
106+
const member = credential.members.find(
107+
(member) => member.userId === data.viewer.id && member.status === 'active'
108+
)
109+
if (
110+
!member ||
111+
member.roleSource !== 'explicit' ||
112+
!validCredentialMembers(
113+
credential,
114+
credential.members.filter((member) => member.userId !== data.viewer.id)
115+
)
116+
)
117+
return
118+
setEdits((current) => ({ ...current, [id]: null }))
119+
},
78120
}
79121
}
Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import styles from '@/platform/app/(shell)/integrations/_components/connection-access/connection-access.module.css'
21
import {
32
SCOPE_GROUPS,
43
scopeGroup,
54
} from '@/platform/app/(shell)/integrations/_components/connection-access/scope-groups'
6-
import { ScrollArea } from '@/platform/design-system/components'
7-
import { CountedLabel } from '@/platform/design-system/components/overflow-text/counted-label'
5+
import { ListField } from '@/platform/design-system/components'
86

97
interface ConnectionAccessProps {
108
permissions: readonly { id: string; label: string; description?: string }[]
@@ -15,41 +13,14 @@ export function ConnectionAccess({
1513
permissions,
1614
title = 'Requested permissions',
1715
}: ConnectionAccessProps) {
18-
if (!permissions.length) return null
1916
return (
20-
<section className={styles.section} aria-label={title}>
21-
<h2 data-type='T059'>
22-
<CountedLabel label={title} count={permissions.length} />
23-
</h2>
24-
<div className={styles.viewport}>
25-
<ScrollArea label={`${title} list`}>
26-
<div className={styles.groups}>
27-
{SCOPE_GROUPS.map((group) => {
28-
const items = permissions.filter(({ id }) => scopeGroup(id) === group)
29-
if (!items.length) return null
30-
return (
31-
<section className={styles.group} key={group} aria-label={group}>
32-
<h3 data-type='T052'>{group}</h3>
33-
<ul className={styles.items}>
34-
{items.map(({ id, label, description }) => (
35-
<li className={styles.item} key={id} data-type='T060'>
36-
<span className={styles.copy}>
37-
<span>{label}</span>
38-
{description && (
39-
<span className={styles.description} data-type='T050'>
40-
{description}
41-
</span>
42-
)}
43-
</span>
44-
</li>
45-
))}
46-
</ul>
47-
</section>
48-
)
49-
})}
50-
</div>
51-
</ScrollArea>
52-
</div>
53-
</section>
17+
<ListField
18+
label={title}
19+
groups={SCOPE_GROUPS.map((group) => ({
20+
id: group,
21+
label: group,
22+
items: permissions.filter(({ id }) => scopeGroup(id) === group),
23+
}))}
24+
/>
5425
)
5526
}

‎apps/sim/platform/app/(shell)/integrations/_components/credential-access/credential-access.module.css‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@
2929
color: var(--ui-secondary);
3030
overflow-wrap: anywhere;
3131
}
32+
.person {
33+
display: grid;
34+
min-inline-size: 0;
35+
gap: var(--ui-stack-gap);
36+
}
3237
.role {
3338
display: inline-flex;
3439
max-inline-size: 100%;
35-
min-inline-size: 0;
40+
min-inline-size: calc(var(--spacing-128) + var(--spacing-32));
41+
white-space: nowrap;
3642
}
3743
@media (max-width: 40rem) {
3844
.invite {

‎apps/sim/platform/app/(shell)/integrations/_components/credential-access/credential-access.tsx‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
Modal,
1515
OverflowText,
1616
Select,
17+
TableLabel,
1718
Tooltip,
1819
} from '@/platform/design-system/components'
19-
import { ActivityBody } from '@/platform/design-system/components/activity-row/activity-body'
2020
import type { IntegrationPerson } from '@/platform/lib/integrations/center-model'
2121
import {
2222
canShareCredential,
@@ -249,18 +249,18 @@ export function CredentialAccess({
249249
selectionDisabled: Boolean(reason),
250250
cells: {
251251
person: (
252-
<ActivityBody
253-
leading={<Avatar name={member.name} src={member.src} size='inline' />}
254-
>
255-
<OverflowText text={member.name} />
256-
{member.email && (
257-
<OverflowText text={member.email}>
258-
<span className={styles.detail} data-type='T050'>
259-
{member.email}
260-
</span>
261-
</OverflowText>
262-
)}
263-
</ActivityBody>
252+
<TableLabel leading={<Avatar name={member.name} src={member.src} size='inline' />}>
253+
<span className={styles.person}>
254+
<OverflowText text={member.name} />
255+
{member.email && (
256+
<OverflowText text={member.email}>
257+
<span className={styles.detail} data-type='T050'>
258+
{member.email}
259+
</span>
260+
</OverflowText>
261+
)}
262+
</span>
263+
</TableLabel>
264264
),
265265
role: (
266266
<Tooltip

‎apps/sim/platform/app/(shell)/integrations/_components/credential-detail/credential-detail.tsx‎

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { type ReactNode, useState } from 'react'
44
import { getErrorMessage } from '@sim/utils/errors'
55
import { useQueryStates } from 'nuqs'
66
import { CredentialAccess } from '@/platform/app/(shell)/integrations/_components/credential-access/credential-access'
@@ -11,7 +11,6 @@ import { ProviderHeading } from '@/platform/app/(shell)/integrations/_components
1111
import {
1212
ActionMenu,
1313
Button,
14-
Chip,
1514
ContentTabs,
1615
TextAreaField,
1716
TextField,
@@ -25,23 +24,25 @@ interface CredentialDetailProps {
2524
integrationName: string
2625
viewer: IntegrationPerson
2726
people: readonly IntegrationPerson[]
28-
onBack: () => void
2927
onChange: (credential: IntegrationCredential) => void
3028
onReconnect?: () => void
3129
onDisconnect: () => void
3230
onLeave: () => void
31+
fields?: ReactNode
32+
onReplace?: () => void
3333
}
3434

3535
export function CredentialDetail({
3636
credential,
3737
integrationName,
3838
viewer,
3939
people,
40-
onBack,
4140
onChange,
4241
onReconnect,
4342
onDisconnect,
4443
onLeave,
44+
fields,
45+
onReplace,
4546
}: CredentialDetailProps) {
4647
const [view, setView] = useQueryStates(credentialDetailParams)
4748
const [displayName, setDisplayName] = useState(credential.displayName)
@@ -56,33 +57,30 @@ export function CredentialDetail({
5657
description={credential.accountLabel ?? integrationName}
5758
providerId={credential.appId}
5859
/>
59-
<IntegrationControls
60-
leading={
61-
<Chip icon='I093' onClick={onBack}>
62-
{integrationName}
63-
</Chip>
64-
}
65-
>
66-
{editable && (
60+
{editable && (
61+
<IntegrationControls leading={null}>
6762
<ActionMenu
6863
label='Credential actions'
6964
icon='I108'
7065
tooltip={false}
7166
actions={[
67+
...(onReplace
68+
? [{ id: 'replace', label: 'Replace value', onSelect: onReplace }]
69+
: []),
7270
...(onReconnect
7371
? [{ id: 'reconnect', label: 'Reconnect', onSelect: onReconnect }]
7472
: []),
7573
{
7674
id: 'disconnect',
77-
label: 'Disconnect',
78-
separatorBefore: Boolean(onReconnect),
75+
label: credential.type === 'secret' ? 'Delete credential' : 'Disconnect',
76+
separatorBefore: Boolean(onReconnect || onReplace),
7977
destructive: true,
8078
onSelect: onDisconnect,
8179
},
8280
]}
8381
/>
84-
)}
85-
</IntegrationControls>
82+
</IntegrationControls>
83+
)}
8684
<ContentTabs
8785
label='Credential details'
8886
current={
@@ -158,6 +156,7 @@ export function CredentialDetail({
158156
viewOnly
159157
/>
160158
)}
159+
{fields}
161160
{credential.scope.kind === 'organization-managed' && (
162161
<p className={styles.managed} data-type='T060'>
163162
Managed by {credential.scope.name}

0 commit comments

Comments
 (0)