Skip to content

Commit f746a06

Browse files
committed
UI/UX cleanup
1 parent 5e9f998 commit f746a06

5 files changed

Lines changed: 431 additions & 300 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
Mail,
2020
Pencil,
2121
Plus,
22+
Rocket,
23+
Shuffle,
2224
SquareArrowUpRight,
2325
Trash,
2426
Unlock,
@@ -68,6 +70,12 @@ interface ContextMenuProps {
6870
onUploadLogo?: () => void
6971
showUploadLogo?: boolean
7072
disableUploadLogo?: boolean
73+
onFork?: () => void
74+
onSync?: () => void
75+
onManage?: () => void
76+
showFork?: boolean
77+
showSync?: boolean
78+
showManage?: boolean
7179
}
7280

7381
/**
@@ -118,6 +126,12 @@ export function ContextMenu({
118126
onUploadLogo,
119127
showUploadLogo = false,
120128
disableUploadLogo = false,
129+
onFork,
130+
onSync,
131+
onManage,
132+
showFork = false,
133+
showSync = false,
134+
showManage = false,
121135
}: ContextMenuProps) {
122136
const hasNavigationSection = showOpenInNewTab && onOpenInNewTab
123137
const hasStatusSection =
@@ -131,6 +145,7 @@ export function ContextMenu({
131145
(showLock && onToggleLock) ||
132146
(showUploadLogo && onUploadLogo)
133147
const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport)
148+
const hasForkSection = (showFork && onFork) || (showSync && onSync) || (showManage && onManage)
134149

135150
return (
136151
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
@@ -294,6 +309,46 @@ export function ContextMenu({
294309
)}
295310

296311
{(hasNavigationSection || hasStatusSection || hasEditSection || hasCopySection) &&
312+
hasForkSection && <DropdownMenuSeparator />}
313+
{showFork && onFork && (
314+
<DropdownMenuItem
315+
onSelect={() => {
316+
onFork()
317+
onClose()
318+
}}
319+
>
320+
<Shuffle />
321+
Fork workspace
322+
</DropdownMenuItem>
323+
)}
324+
{showSync && onSync && (
325+
<DropdownMenuItem
326+
onSelect={() => {
327+
onSync()
328+
onClose()
329+
}}
330+
>
331+
<Rocket />
332+
Sync workspace
333+
</DropdownMenuItem>
334+
)}
335+
{showManage && onManage && (
336+
<DropdownMenuItem
337+
onSelect={() => {
338+
onManage()
339+
onClose()
340+
}}
341+
>
342+
<Shuffle />
343+
Manage forks
344+
</DropdownMenuItem>
345+
)}
346+
347+
{(hasNavigationSection ||
348+
hasStatusSection ||
349+
hasEditSection ||
350+
hasCopySection ||
351+
hasForkSection) &&
297352
(showLeave || showDelete) && <DropdownMenuSeparator />}
298353
{showLeave && onLeave && (
299354
<DropdownMenuItem

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal.tsx

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

3-
import { useEffect, useMemo, useState } from 'react'
4-
import { ChevronRight, Search } from 'lucide-react'
3+
import { useEffect, useId, useMemo, useState } from 'react'
4+
import { Search } from 'lucide-react'
55
import { useRouter } from 'next/navigation'
66
import {
77
Checkbox,
8+
ChevronDown,
9+
ChipCopyInput,
810
ChipInput,
911
ChipModal,
1012
ChipModalBody,
1113
ChipModalError,
12-
ChipModalField,
1314
ChipModalFooter,
1415
ChipModalHeader,
1516
toast,
@@ -19,6 +20,7 @@ import type {
1920
GetForkResourcesResponse,
2021
} from '@/lib/api/contracts/workspace-fork'
2122
import { cn } from '@/lib/core/utils/cn'
23+
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
2224
import { useForkResources, useForkWorkspace } from '@/hooks/queries/workspace-fork'
2325

2426
interface ForkWorkspaceModalProps {
@@ -74,6 +76,7 @@ function ResourceKindRow({
7476
}) {
7577
const [expanded, setExpanded] = useState(false)
7678
const [query, setQuery] = useState('')
79+
const fieldId = useId()
7780

7881
const total = items.length
7982
const selectedCount = selected.size
@@ -97,18 +100,18 @@ function ResourceKindRow({
97100
/>
98101
<button
99102
type='button'
100-
className='flex min-w-0 items-center gap-1 text-left hover:text-[var(--text-primary)]'
103+
className='flex min-w-0 flex-1 items-center gap-1 text-left hover:text-[var(--text-primary)]'
101104
onClick={() => setExpanded((value) => !value)}
102105
>
103-
<ChevronRight
106+
<span className='min-w-0 flex-1 truncate'>
107+
{label} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
108+
</span>
109+
<ChevronDown
104110
className={cn(
105-
'size-[14px] text-[var(--text-icon)] transition-transform',
106-
expanded && 'rotate-90'
111+
'h-[6px] w-[10px] flex-shrink-0 text-[var(--text-icon)] transition-transform',
112+
expanded && 'rotate-180'
107113
)}
108114
/>
109-
<span className='truncate'>
110-
{label} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
111-
</span>
112115
</button>
113116
</div>
114117

@@ -126,23 +129,27 @@ function ResourceKindRow({
126129
<div className='flex max-h-44 flex-col gap-0.5 overflow-y-auto'>
127130
{filtered.map((item) => {
128131
const isChecked = selected.has(item.id)
132+
const itemId = `${fieldId}-${item.id}`
129133
return (
130-
<button
134+
<label
131135
key={item.id}
132-
type='button'
133-
className='flex min-w-0 items-center gap-2 rounded-md py-0.5 text-left text-[var(--text-body)] text-sm hover:text-[var(--text-primary)]'
134-
onClick={() => onToggleItem(item.id, !isChecked)}
135-
disabled={disabled}
136+
htmlFor={itemId}
137+
className={cn(
138+
'flex min-w-0 items-center gap-2 rounded-md py-0.5 text-[var(--text-body)] text-sm',
139+
disabled
140+
? 'cursor-not-allowed opacity-60'
141+
: 'cursor-pointer hover:text-[var(--text-primary)]'
142+
)}
136143
>
137144
<Checkbox
145+
id={itemId}
138146
size='sm'
139147
checked={isChecked}
140-
aria-hidden
141-
tabIndex={-1}
142-
className='pointer-events-none'
148+
onCheckedChange={(checked) => onToggleItem(item.id, checked === true)}
149+
disabled={disabled}
143150
/>
144151
<span className='truncate'>{item.label}</span>
145-
</button>
152+
</label>
146153
)
147154
})}
148155
{filtered.length === 0 ? (
@@ -224,58 +231,77 @@ export function ForkWorkspaceModal({
224231
<ChipModal open={open} onOpenChange={onOpenChange} srTitle='Fork workspace'>
225232
<ChipModalHeader onClose={() => onOpenChange(false)}>Fork workspace</ChipModalHeader>
226233
<ChipModalBody>
227-
<ChipModalField type='copy' title='Forking from' value={sourceWorkspaceName} />
228-
<ChipModalField
229-
type='input'
230-
title='Name'
231-
value={name}
232-
onChange={setName}
233-
placeholder='Workspace name'
234-
maxLength={100}
235-
autoComplete='off'
236-
disabled={isForking}
237-
onSubmit={handleSubmit}
238-
required
239-
/>
240-
{availableKinds.length > 0 ? (
241-
<ChipModalField type='custom' title='Copy resources'>
242-
<div className='flex flex-col gap-2'>
243-
{availableKinds.map((kind) => (
244-
<ResourceKindRow
245-
key={kind.key}
246-
label={kind.label}
247-
items={resources.data?.[kind.key] ?? []}
248-
selected={selected[kind.key]}
249-
onToggleAll={(selectAll) =>
250-
setSelected((prev) => ({
251-
...prev,
252-
[kind.key]: selectAll
253-
? new Set((resources.data?.[kind.key] ?? []).map((item) => item.id))
254-
: new Set<string>(),
255-
}))
256-
}
257-
onToggleItem={(id, checked) =>
258-
setSelected((prev) => {
259-
const next = new Set(prev[kind.key])
260-
if (checked) next.add(id)
261-
else next.delete(id)
262-
return { ...prev, [kind.key]: next }
263-
})
264-
}
265-
disabled={isForking}
266-
/>
267-
))}
268-
<p className='text-[var(--text-muted)] text-caption'>
269-
Unselected resources leave their workflow fields empty in the fork.
270-
</p>
271-
</div>
272-
</ChipModalField>
273-
) : null}
274-
{noDeployedWorkflows ? (
275-
<p className='px-2 text-[var(--text-muted)] text-caption'>
276-
No deployed workflows to copy — your fork will start with a blank workflow.
277-
</p>
278-
) : null}
234+
<div className='flex flex-col gap-7 px-2'>
235+
<SettingsSection label='Forking from'>
236+
<ChipCopyInput value={sourceWorkspaceName} aria-label='Forking from' />
237+
</SettingsSection>
238+
239+
<SettingsSection
240+
label='Name'
241+
headerAccessory={
242+
<span className='text-[var(--text-error)]' title='Required'>
243+
*
244+
</span>
245+
}
246+
>
247+
<ChipInput
248+
value={name}
249+
onChange={(event) => setName(event.target.value)}
250+
onKeyDown={(event) => {
251+
if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
252+
event.preventDefault()
253+
handleSubmit()
254+
}
255+
}}
256+
placeholder='Workspace name'
257+
maxLength={100}
258+
autoComplete='off'
259+
disabled={isForking}
260+
aria-label='Workspace name'
261+
/>
262+
</SettingsSection>
263+
264+
{availableKinds.length > 0 ? (
265+
<SettingsSection label='Copy resources'>
266+
<div className='flex flex-col gap-2'>
267+
{availableKinds.map((kind) => (
268+
<ResourceKindRow
269+
key={kind.key}
270+
label={kind.label}
271+
items={resources.data?.[kind.key] ?? []}
272+
selected={selected[kind.key]}
273+
onToggleAll={(selectAll) =>
274+
setSelected((prev) => ({
275+
...prev,
276+
[kind.key]: selectAll
277+
? new Set((resources.data?.[kind.key] ?? []).map((item) => item.id))
278+
: new Set<string>(),
279+
}))
280+
}
281+
onToggleItem={(id, checked) =>
282+
setSelected((prev) => {
283+
const next = new Set(prev[kind.key])
284+
if (checked) next.add(id)
285+
else next.delete(id)
286+
return { ...prev, [kind.key]: next }
287+
})
288+
}
289+
disabled={isForking}
290+
/>
291+
))}
292+
<p className='text-[var(--text-muted)] text-caption'>
293+
Unselected resources leave their workflow fields empty in the fork.
294+
</p>
295+
</div>
296+
</SettingsSection>
297+
) : null}
298+
299+
{noDeployedWorkflows ? (
300+
<p className='text-[var(--text-muted)] text-caption'>
301+
No deployed workflows to copy — your fork will start with a blank workflow.
302+
</p>
303+
) : null}
304+
</div>
279305
<ChipModalError>{error ?? undefined}</ChipModalError>
280306
</ChipModalBody>
281307
<ChipModalFooter

0 commit comments

Comments
 (0)