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'
55import { useRouter } from 'next/navigation'
66import {
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'
2122import { cn } from '@/lib/core/utils/cn'
23+ import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
2224import { useForkResources , useForkWorkspace } from '@/hooks/queries/workspace-fork'
2325
2426interface 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