Skip to content

Commit bb4c7f4

Browse files
committed
refactor(workspace-settings): sandboxes, teammates and recently deleted on the shared settings parts
The sandbox editor owns its draft and mutations, keyed by the sandbox it opened, which retires the draft-owner state set during render and the eighteen drilled props; create opens through useSettingsSubview and submits through Create sandbox, edits through SettingsSaveBar. Teammates lists members and invites under one SettingsList ending in Invite teammates, whose button is disabled with its reason, or reads View plans where a plan limit closes it. Recently deleted merges restored rows without splicing during render and labels types in sentence case.
1 parent 2c95e32 commit bb4c7f4

7 files changed

Lines changed: 345 additions & 398 deletions

File tree

‎apps/sim/app/(product)/workspace/[workspaceId]/settings/_components/sections/recently-deleted/recently-deleted-section.tsx‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ const SORT_COLUMNS = [
7373
const TYPE_LABEL: Record<DeletedType, string> = {
7474
workflow: 'Workflow',
7575
folder: 'Folder',
76-
workspace_folder: 'File Folder',
77-
knowledge_folder: 'Knowledge Folder',
78-
table_folder: 'Table Folder',
76+
workspace_folder: 'File folder',
77+
knowledge_folder: 'Knowledge folder',
78+
table_folder: 'Table folder',
7979
table: 'Table',
80-
knowledge: 'Knowledge Base',
80+
knowledge: 'Knowledge base',
8181
file: 'File',
8282
}
8383

@@ -161,7 +161,7 @@ export default function RecentlyDeletedSection({ workspaceId }: WorkspaceSetting
161161
const restoreWorkspaceFile = useRestoreWorkspaceFile()
162162
const restoreWorkspaceFileFolder = useRestoreWorkspaceFileFolder()
163163

164-
const items: DeletedResource[] = (deleted.data?.pages ?? []).flatMap((page) =>
164+
const fetched: DeletedResource[] = (deleted.data?.pages ?? []).flatMap((page) =>
165165
page.items.filter(isWorkspaceResource).map((item) => ({
166166
id: item.id,
167167
name: item.name,
@@ -170,13 +170,25 @@ export default function RecentlyDeletedSection({ workspaceId }: WorkspaceSetting
170170
}))
171171
)
172172
const normalized = debouncedSearch.toLowerCase()
173-
const listed = new Set(items.map((item) => item.id))
174-
for (const [id, entry] of restored) {
175-
if (listed.has(id)) continue
176-
if (!RECENTLY_DELETED_TYPES_BY_FILTER[tab].includes(entry.resource.type)) continue
177-
if (normalized && !entry.resource.name.toLowerCase().includes(normalized)) continue
178-
items.splice(Math.min(entry.displayIndex, items.length), 0, entry.resource)
179-
}
173+
const listed = new Set(fetched.map((item) => item.id))
174+
/** Restored rows hold their place until the list refetches without them. */
175+
const items = [...restored.entries()]
176+
.filter(
177+
([id, entry]) =>
178+
!listed.has(id) &&
179+
RECENTLY_DELETED_TYPES_BY_FILTER[tab].includes(entry.resource.type) &&
180+
(!normalized || entry.resource.name.toLowerCase().includes(normalized))
181+
)
182+
.map(([, entry]) => entry)
183+
.sort((left, right) => left.displayIndex - right.displayIndex)
184+
.reduce<DeletedResource[]>(
185+
(list, entry) => [
186+
...list.slice(0, entry.displayIndex),
187+
entry.resource,
188+
...list.slice(entry.displayIndex),
189+
],
190+
fetched
191+
)
180192

181193
const restore = async (resource: DeletedResource) => {
182194
const displayIndex = Math.max(

‎apps/sim/app/(product)/workspace/[workspaceId]/settings/_components/sections/sandboxes/sandbox-editor.tsx‎

Lines changed: 137 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { SelectField, TextAreaField, TextField } from '@sim/emcn/field'
77
import { ConfirmDialog } from '@sim/emcn/modal/confirm-dialog'
88
import { MultiSelect } from '@sim/emcn/picker/multi-select'
99
import { SettingsRow, SettingsSection } from '@sim/emcn/settings-section'
10-
import { SaveActions } from '@sim/emcn/settings-section/save-actions'
1110
import { InlineNotice, StatusTag } from '@sim/emcn/status'
11+
import { getErrorMessage } from '@sim/utils/errors'
12+
import { toast } from '@/lib/toast/sink'
13+
import { SettingsSaveBar } from '@/app/(product)/_components/settings-parts/save-bar'
1214
import {
15+
ActionRow,
1316
DetailHeader,
1417
FieldStack,
1518
FormFieldset,
@@ -19,14 +22,23 @@ import {
1922
} from '@/app/(product)/_components/settings-parts/settings-parts'
2023
import {
2124
CLI_TOOL_OPTIONS,
25+
canRetrySandboxBuild,
2226
DEPENDENCY_PLACEHOLDERS,
27+
draftFromSandbox,
28+
emptyDraft,
29+
extractIssues,
2330
LANGUAGE_OPTIONS,
2431
type Sandbox,
2532
type SandboxCliToolId,
2633
type SandboxDependencyIssue,
2734
type SandboxDraft,
2835
SYSTEM_PACKAGE_PLACEHOLDER,
36+
toSubmittedLines,
37+
useCreateSandbox,
38+
useDeleteSandbox,
39+
useUpdateSandbox,
2940
} from '@/platform/lib/data/sandboxes'
41+
import { requestSettingsLeave, useSettingsUnsavedGuard } from '@/platform/lib/data/settings-guard'
3042
import { plural } from '@/platform/shared/plural'
3143

3244
/** One line per rejected row, the first rejection of each line only. */
@@ -44,10 +56,10 @@ const STATUS = {
4456
pending: { label: 'Queued', intent: 'info' },
4557
} as const
4658

47-
function contentsSummary(sandbox: Sandbox): string {
48-
const dependencies = sandbox.dependencies.length
59+
/** What a sandbox installs: `3 packages · 1 system package · 2 managed CLIs`. */
60+
export function contentsSummary(sandbox: Sandbox): string {
4961
return [
50-
`${dependencies} ${dependencies === 1 ? 'dependency' : 'dependencies'}`,
62+
plural(sandbox.dependencies.length, 'package'),
5163
plural(sandbox.systemPackages.length, 'system package'),
5264
plural(sandbox.cliTools.length, 'managed CLI'),
5365
].join(' · ')
@@ -73,8 +85,8 @@ function SandboxStatus({
7385
return (
7486
<FieldStack>
7587
<Muted>
76-
Dependencies, system packages, and managed CLI tools install at run time on this
77-
deployment, adding startup time per execution. Prebuilt sandboxes require E2B.
88+
Packages and managed CLI tools install at run time on this deployment, adding startup time
89+
to each run. Prebuilt sandboxes need E2B
7890
</Muted>
7991
</FieldStack>
8092
)
@@ -86,8 +98,8 @@ function SandboxStatus({
8698
return (
8799
<FieldStack>
88100
<Muted>
89-
No dependencies, system packages, or managed CLI tools yet. Add one above to build this
90-
sandbox; until then it runs on the Function base.
101+
Nothing to install yet. Add a package or managed CLI tool above to build this sandbox;
102+
until then it runs on the Function base
91103
</Muted>
92104
</FieldStack>
93105
)
@@ -123,53 +135,105 @@ function SandboxStatus({
123135
}
124136

125137
interface SandboxEditorProps {
126-
/** The saved sandbox, absent while creating one. */
138+
workspaceId: string
139+
/** The saved sandbox, absent while creating one. The list keys the editor by it. */
127140
sandbox: Sandbox | null
128141
strategy: 'prebuilt' | 'runtime'
129-
draft: SandboxDraft
130-
onChange: (draft: SandboxDraft) => void
131-
dependencyIssues: SandboxDependencyIssue[]
132-
systemPackageIssues: SandboxDependencyIssue[]
133142
canAdmin: boolean
134-
dirty: boolean
135-
saving: boolean
136-
deleting: boolean
137-
retrying: boolean
138-
canRetry: boolean
139-
onBack: () => void
140-
onSave: () => void
141-
onDiscard: () => void
142-
onRetry: () => void
143-
onDelete: () => Promise<void>
143+
/** Back to the list, after a save, a delete or the back action. */
144+
onClose: () => void
144145
}
145146

146-
/** A sandbox's packages, managed CLI tools and build, or a new one's form. */
147+
/**
148+
* A sandbox's packages, managed CLI tools and build, or a new one's form. It holds its own draft,
149+
* seeded from the sandbox it was keyed by, so opening another sandbox never carries edits over.
150+
*/
147151
export function SandboxEditor({
152+
workspaceId,
148153
sandbox,
149154
strategy,
150-
draft,
151-
onChange,
152-
dependencyIssues,
153-
systemPackageIssues,
154155
canAdmin,
155-
dirty,
156-
saving,
157-
deleting,
158-
retrying,
159-
canRetry,
160-
onBack,
161-
onSave,
162-
onDiscard,
163-
onRetry,
164-
onDelete,
156+
onClose,
165157
}: SandboxEditorProps) {
158+
const createSandbox = useCreateSandbox()
159+
const updateSandbox = useUpdateSandbox()
160+
const deleteSandbox = useDeleteSandbox()
161+
const [original] = useState(() => (sandbox ? draftFromSandbox(sandbox) : emptyDraft()))
162+
const [draft, setDraft] = useState<SandboxDraft>(original)
163+
const [dependencyIssues, setDependencyIssues] = useState<SandboxDependencyIssue[]>([])
164+
const [systemPackageIssues, setSystemPackageIssues] = useState<SandboxDependencyIssue[]>([])
166165
const [confirmingDelete, setConfirmingDelete] = useState(false)
166+
167+
const dirty =
168+
draft.name.trim() !== original.name.trim() ||
169+
draft.language !== original.language ||
170+
draft.dependencies !== original.dependencies ||
171+
draft.systemPackages !== original.systemPackages ||
172+
draft.cliTools.join(',') !== original.cliTools.join(',')
173+
const saving = createSandbox.isPending || updateSandbox.isPending
167174
const disabled = !canAdmin || saving
175+
const canRetry = canRetrySandboxBuild({ canAdmin, isDirty: dirty, saving })
176+
177+
const discard = () => {
178+
setDraft(original)
179+
setDependencyIssues([])
180+
setSystemPackageIssues([])
181+
}
182+
183+
/** The save bar registers an edit's draft; the create form, which has none, registers here. */
184+
useSettingsUnsavedGuard({
185+
isDirty: !sandbox && dirty,
186+
navigationBlocked: deleteSandbox.isPending || (!sandbox && saving),
187+
onDiscard: sandbox ? undefined : discard,
188+
})
189+
190+
async function save() {
191+
setDependencyIssues([])
192+
setSystemPackageIssues([])
193+
const body = {
194+
name: draft.name.trim(),
195+
language: draft.language,
196+
dependencies: toSubmittedLines(draft.dependencies),
197+
systemPackages: toSubmittedLines(draft.systemPackages),
198+
cliTools: draft.cliTools,
199+
}
200+
try {
201+
if (sandbox) await updateSandbox.mutateAsync({ workspaceId, sandboxId: sandbox.id, ...body })
202+
else await createSandbox.mutateAsync({ workspaceId, ...body })
203+
onClose()
204+
} catch (caught) {
205+
const lineIssues = extractIssues(caught)
206+
if (lineIssues) {
207+
setDependencyIssues(lineIssues.field === 'dependencies' ? lineIssues.issues : [])
208+
setSystemPackageIssues(lineIssues.field === 'systemPackages' ? lineIssues.issues : [])
209+
return
210+
}
211+
toast.error(getErrorMessage(caught, 'Failed to save sandbox'))
212+
}
213+
}
214+
215+
async function retry() {
216+
if (!sandbox || !canRetry) return
217+
try {
218+
await updateSandbox.mutateAsync({
219+
workspaceId,
220+
sandboxId: sandbox.id,
221+
language: sandbox.language,
222+
dependencies: sandbox.dependencies,
223+
systemPackages: sandbox.systemPackages,
224+
cliTools: sandbox.cliTools,
225+
})
226+
toast.success('Sandbox build queued')
227+
} catch (caught) {
228+
toast.error(getErrorMessage(caught, 'Failed to retry sandbox build'))
229+
}
230+
}
231+
168232
return (
169233
<SectionStack>
170234
<DetailHeader
171235
backLabel='Sandboxes'
172-
onBack={onBack}
236+
onBack={() => requestSettingsLeave(onClose)}
173237
title={sandbox ? sandbox.name : 'New sandbox'}
174238
description='Dependencies, system packages and managed CLI tools your Function blocks can use'
175239
/>
@@ -179,7 +243,7 @@ export function SandboxEditor({
179243
<TextField
180244
label='Name'
181245
value={draft.name}
182-
onChange={(name) => onChange({ ...draft, name })}
246+
onChange={(name) => setDraft({ ...draft, name })}
183247
placeholder='bigquery-etl'
184248
maxLength={64}
185249
autoComplete='off'
@@ -188,7 +252,7 @@ export function SandboxEditor({
188252
<SelectField
189253
label='Language'
190254
value={draft.language}
191-
onValueChange={(language) => onChange({ ...draft, language })}
255+
onValueChange={(language) => setDraft({ ...draft, language })}
192256
options={LANGUAGE_OPTIONS}
193257
disabled={disabled}
194258
/>
@@ -199,7 +263,7 @@ export function SandboxEditor({
199263
<TextAreaField
200264
label='Dependencies'
201265
value={draft.dependencies}
202-
onChange={(dependencies) => onChange({ ...draft, dependencies })}
266+
onChange={(dependencies) => setDraft({ ...draft, dependencies })}
203267
placeholder={DEPENDENCY_PLACEHOLDERS[draft.language]}
204268
defaultSize='expanded'
205269
spellCheck={false}
@@ -212,7 +276,7 @@ export function SandboxEditor({
212276
options={CLI_TOOL_OPTIONS}
213277
value={draft.cliTools}
214278
onValueChange={(cliTools) =>
215-
onChange({ ...draft, cliTools: cliTools as SandboxCliToolId[] })
279+
setDraft({ ...draft, cliTools: cliTools as SandboxCliToolId[] })
216280
}
217281
placeholder='Select managed CLI tools'
218282
searchLabel='Search managed CLI tools'
@@ -221,7 +285,7 @@ export function SandboxEditor({
221285
<TextAreaField
222286
label='System packages'
223287
value={draft.systemPackages}
224-
onChange={(systemPackages) => onChange({ ...draft, systemPackages })}
288+
onChange={(systemPackages) => setDraft({ ...draft, systemPackages })}
225289
placeholder={SYSTEM_PACKAGE_PLACEHOLDER}
226290
defaultSize='expanded'
227291
spellCheck={false}
@@ -237,29 +301,42 @@ export function SandboxEditor({
237301
<SandboxStatus
238302
sandbox={sandbox}
239303
strategy={strategy}
240-
onRetry={canAdmin ? onRetry : undefined}
241-
retrying={retrying}
304+
onRetry={canAdmin ? () => void retry() : undefined}
305+
retrying={updateSandbox.isPending}
242306
retryDisabled={!canRetry}
243307
/>
244308
</SettingsSection>
245309
)}
246-
<SaveActions
247-
dirty={dirty}
248-
saving={saving}
249-
blocked={!canAdmin || draft.name.trim().length === 0}
250-
onSave={onSave}
251-
onDiscard={onDiscard}
252-
/>
310+
{canAdmin &&
311+
(sandbox ? (
312+
<SettingsSaveBar
313+
dirty={dirty}
314+
saving={saving}
315+
blocked={draft.name.trim().length === 0}
316+
onSave={() => void save()}
317+
onDiscard={discard}
318+
/>
319+
) : (
320+
<ActionRow>
321+
<Button
322+
variant='primary'
323+
disabled={saving || draft.name.trim().length === 0}
324+
onClick={() => void save()}
325+
>
326+
{saving ? 'Creating…' : 'Create sandbox'}
327+
</Button>
328+
</ActionRow>
329+
))}
253330
{sandbox && canAdmin && (
254331
<SettingsSection title='Manage sandbox'>
255332
<SettingsRow label='Delete sandbox' description='Remove this sandbox from the workspace'>
256333
<Button
257334
variant='quiet'
258335
destructive
259-
disabled={deleting}
336+
disabled={deleteSandbox.isPending}
260337
onClick={() => setConfirmingDelete(true)}
261338
>
262-
{deleting ? 'Deleting…' : 'Delete'}
339+
Delete
263340
</Button>
264341
</SettingsRow>
265342
</SettingsSection>
@@ -269,10 +346,13 @@ export function SandboxEditor({
269346
target={confirmingDelete}
270347
onClose={() => setConfirmingDelete(false)}
271348
title='Delete sandbox'
272-
description={`This will permanently delete ${sandbox.name} and remove it from any Function blocks that are using it. This action cannot be undone.`}
349+
description={`${sandbox.name} is removed from every Function block that uses it. This action cannot be undone.`}
273350
confirmLabel='Delete'
274351
pendingLabel='Deleting…'
275-
onConfirm={onDelete}
352+
onConfirm={async () => {
353+
await deleteSandbox.mutateAsync({ workspaceId, sandboxId: sandbox.id })
354+
onClose()
355+
}}
276356
/>
277357
)}
278358
</SectionStack>

0 commit comments

Comments
 (0)