11import { getRouteApi , useRouteContext } from '@tanstack/react-router' ;
2+ import { Array , HashMap , Match , Option , pipe , Predicate } from 'effect' ;
23import { Ulid } from 'id128' ;
34import { Suspense , useState } from 'react' ;
45import {
5- Collection ,
6+ ListBox as AriaListBox ,
7+ ListBoxItem as AriaListBoxItem ,
68 Dialog ,
79 DialogTrigger ,
810 Key ,
911 MenuTrigger ,
10- Tab ,
11- TabList ,
12- TabPanel ,
13- Tabs ,
12+ ToggleButton ,
1413 Tooltip ,
1514 TooltipTrigger ,
15+ useDragAndDrop ,
1616} from 'react-aria-components' ;
1717import { FiMoreHorizontal , FiPlus } from 'react-icons/fi' ;
1818import { twJoin } from 'tailwind-merge' ;
19-
2019import { EnvironmentListItem } from '@the-dev-tools/spec/environment/v1/environment_pb' ;
2120import {
2221 EnvironmentCreateEndpoint ,
2322 EnvironmentDeleteEndpoint ,
2423 EnvironmentListEndpoint ,
24+ EnvironmentMoveEndpoint ,
2525 EnvironmentUpdateEndpoint ,
2626} from '@the-dev-tools/spec/meta/environment/v1/environment.endpoints.ts' ;
2727import {
@@ -35,6 +35,7 @@ import {
3535 WorkspaceGetEndpoint ,
3636 WorkspaceUpdateEndpoint ,
3737} from '@the-dev-tools/spec/meta/workspace/v1/workspace.endpoints.ts' ;
38+ import { MovePosition } from '@the-dev-tools/spec/resources/v1/resources_pb' ;
3839import { Button } from '@the-dev-tools/ui/button' ;
3940import { DataTable , useReactTable } from '@the-dev-tools/ui/data-table' ;
4041import { GlobalEnvironmentIcon , Spinner , VariableIcon } from '@the-dev-tools/ui/icons' ;
@@ -45,7 +46,6 @@ import { Select } from '@the-dev-tools/ui/select';
4546import { tw } from '@the-dev-tools/ui/tailwind-literal' ;
4647import { TextField , useEditableTextState } from '@the-dev-tools/ui/text-field' ;
4748import { useMutate , useQuery } from '~data-client' ;
48-
4949import {
5050 columnActionsCommon ,
5151 columnCheckboxField ,
@@ -126,25 +126,85 @@ const EnvironmentModal = () => {
126126
127127 const { items : environments } = useQuery ( EnvironmentListEndpoint , { workspaceId } ) ;
128128
129- const [ selectedKey , setSelectedKey ] = useState < Key | null > ( null ) ;
129+ const environmentMap = pipe (
130+ Array . map ( environments , ( _ ) => [ Ulid . construct ( _ . environmentId ) . toCanonical ( ) , _ ] as const ) ,
131+ HashMap . fromIterable ,
132+ ) ;
133+
134+ const { global : [ global ] = [ ] , rest = [ ] } = Array . groupBy ( environments , ( _ ) => ( _ . isGlobal ? 'global' : 'rest' ) ) ;
135+
136+ const globalIdCan = pipe (
137+ Option . fromNullable ( global ) ,
138+ Option . map ( ( _ ) => Ulid . construct ( _ . environmentId ) . toCanonical ( ) ) ,
139+ Option . getOrUndefined ,
140+ ) ;
141+
142+ const [ selectedKey , setSelectedKey ] = useState < Key | undefined > ( globalIdCan ) ;
143+
144+ const environment = pipe (
145+ Option . liftPredicate ( selectedKey , Predicate . isString ) ,
146+ Option . flatMap ( ( _ ) => HashMap . get ( environmentMap , _ ) ) ,
147+ Option . getOrNull ,
148+ ) ;
149+
150+ const { dragAndDropHooks } = useDragAndDrop ( {
151+ getItems : ( keys ) => [ ...keys ] . map ( ( key ) => ( { key : key . toString ( ) } ) ) ,
152+ onReorder : ( { keys, target : { dropPosition, key } } ) =>
153+ Option . gen ( function * ( ) {
154+ const targetIdCan = yield * Option . liftPredicate ( key , Predicate . isString ) ;
155+
156+ const sourceIdCan = yield * pipe (
157+ yield * Option . liftPredicate ( keys , ( _ ) => _ . size === 1 ) ,
158+ Array . fromIterable ,
159+ Array . head ,
160+ Option . filter ( Predicate . isString ) ,
161+ ) ;
162+
163+ const position = yield * pipe (
164+ Match . value ( dropPosition ) ,
165+ Match . when ( 'after' , ( ) => MovePosition . AFTER ) ,
166+ Match . when ( 'before' , ( ) => MovePosition . BEFORE ) ,
167+ Match . option ,
168+ ) ;
169+
170+ void dataClient . fetch ( EnvironmentMoveEndpoint , {
171+ environmentId : Ulid . fromCanonical ( sourceIdCan ) . bytes ,
172+ position,
173+ targetEnvironmentId : Ulid . fromCanonical ( targetIdCan ) . bytes ,
174+ workspaceId,
175+ } ) ;
176+ } ) ,
177+ renderDropIndicator : ( ) => < div className = { tw `relative z-10 h-0 w-full ring ring-violet-700` } /> ,
178+ } ) ;
130179
131180 return (
132181 < Modal >
133182 < Dialog className = { tw `h-full outline-hidden` } >
134183 { ( { close } ) => (
135- < Tabs
136- className = { tw `flex h-full` }
137- onSelectionChange = { setSelectedKey }
138- orientation = 'vertical'
139- selectedKey = { selectedKey }
140- >
184+ < div className = { tw `flex h-full` } >
141185 < div className = { tw `flex w-64 flex-col border-r border-slate-200 bg-slate-50 p-4 tracking-tight` } >
142- < div className = { tw `-order-3 mb-4` } >
186+ < div className = { tw `mb-4` } >
143187 < div className = { tw `mb-0.5 text-sm leading-5 font-semibold text-slate-800` } > Variable Settings</ div >
144188 < div className = { tw `text-xs leading-4 text-slate-500` } > Manage variables & environment </ div >
145189 </ div >
146190
147- < div className = { tw `-order-1 mt-3 mb-1 flex items-center justify-between py-0.5` } >
191+ < ToggleButton
192+ className = { ( { isSelected } ) =>
193+ twJoin (
194+ tw `-mx-2 flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm` ,
195+ isSelected && tw `bg-slate-200` ,
196+ )
197+ }
198+ isSelected = { selectedKey === globalIdCan }
199+ onChange = { ( isSelected ) => {
200+ if ( isSelected && globalIdCan ) setSelectedKey ( globalIdCan ) ;
201+ } }
202+ >
203+ < VariableIcon className = { tw `size-4 text-slate-500` } />
204+ < span className = { tw `text-md leading-5 font-semibold` } > Global Variables</ span >
205+ </ ToggleButton >
206+
207+ < div className = { tw `mt-3 mb-1 flex items-center justify-between py-0.5` } >
148208 < span className = { tw `text-md leading-5 text-slate-400` } > Environments</ span >
149209
150210 < TooltipTrigger delay = { 750 } >
@@ -170,58 +230,52 @@ const EnvironmentModal = () => {
170230 </ TooltipTrigger >
171231 </ div >
172232
173- < TabList className = { tw `contents` } items = { environments } >
174- { ( item ) => {
175- const environmentIdCan = Ulid . construct ( item . environmentId ) . toCanonical ( ) ;
176- return (
177- < Tab
178- className = { ( { isSelected } ) =>
179- twJoin (
180- tw `-mx-2 flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm` ,
181- isSelected && tw `bg-slate-200` ,
182- item . isGlobal && tw `-order-2` ,
183- )
184- }
185- id = { environmentIdCan }
186- >
187- { item . isGlobal ? (
188- < VariableIcon className = { tw `size-4 text-slate-500` } />
189- ) : (
190- < div
191- className = { tw `
192- flex size-4 items-center justify-center rounded-sm bg-slate-300 text-xs leading-3
193- text-slate-500
194- ` }
195- >
196- { item . name [ 0 ] }
197- </ div >
198- ) }
199- < span className = { tw `text-md leading-5 font-semibold` } >
200- { item . isGlobal ? 'Global Variables' : item . name }
201- </ span >
202- </ Tab >
203- ) ;
233+ < AriaListBox
234+ aria-label = 'Environments'
235+ dragAndDropHooks = { dragAndDropHooks }
236+ items = { rest }
237+ onSelectionChange = { ( keys ) => {
238+ if ( ! Predicate . isSet ( keys ) || keys . size !== 1 ) return ;
239+ const [ key ] = keys . values ( ) ;
240+ setSelectedKey ( key ) ;
204241 } }
205- </ TabList >
242+ selectedKeys = { Array . fromNullable ( selectedKey ) }
243+ selectionMode = 'single'
244+ >
245+ { ( _ ) => (
246+ < AriaListBoxItem
247+ className = { ( { isSelected } ) =>
248+ twJoin (
249+ tw `-mx-2 flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm` ,
250+ isSelected && tw `bg-slate-200` ,
251+ )
252+ }
253+ id = { Ulid . construct ( _ . environmentId ) . toCanonical ( ) }
254+ textValue = { _ . name }
255+ >
256+ < div
257+ className = { tw `
258+ flex size-4 items-center justify-center rounded-sm bg-slate-300 text-xs leading-3 text-slate-500
259+ ` }
260+ >
261+ { _ . name [ 0 ] }
262+ </ div >
263+ < span className = { tw `text-md leading-5 font-semibold` } > { _ . name } </ span >
264+ </ AriaListBoxItem >
265+ ) }
266+ </ AriaListBox >
206267 </ div >
207268
208269 < div className = { tw `flex h-full min-w-0 flex-1 flex-col` } >
209- < Collection items = { environments } >
210- { ( _ ) => {
211- const id = Ulid . construct ( _ . environmentId ) . toCanonical ( ) ;
212- return < EnvironmentPanel environment = { _ } id = { id } /> ;
213- } }
214- </ Collection >
215-
270+ { environment && < EnvironmentPanel environment = { environment } /> }
216271 < div className = { tw `flex-1` } />
217-
218272 < div className = { tw `flex justify-end gap-2 border-t border-slate-200 px-6 py-3` } >
219273 < Button onPress = { close } variant = 'primary' >
220274 Close
221275 </ Button >
222276 </ div >
223277 </ div >
224- </ Tabs >
278+ </ div >
225279 ) }
226280 </ Dialog >
227281 </ Modal >
@@ -230,10 +284,9 @@ const EnvironmentModal = () => {
230284
231285interface EnvironmentPanelProps {
232286 environment : EnvironmentListItem ;
233- id : string ;
234287}
235288
236- const EnvironmentPanel = ( { environment : { environmentId, isGlobal, name } , id } : EnvironmentPanelProps ) => {
289+ const EnvironmentPanel = ( { environment : { environmentId, isGlobal, name } } : EnvironmentPanelProps ) => {
237290 const { dataClient } = useRouteContext ( { from : '__root__' } ) ;
238291
239292 const [ environmentUpdate , environmentUpdateLoading ] = useMutate ( EnvironmentUpdateEndpoint ) ;
@@ -246,7 +299,7 @@ const EnvironmentPanel = ({ environment: { environmentId, isGlobal, name }, id }
246299 } ) ;
247300
248301 return (
249- < TabPanel className = { tw `h-full px-6 py-4` } id = { id } >
302+ < div className = { tw `h-full px-6 py-4` } >
250303 < div className = { tw `mb-4 flex items-center gap-2` } onContextMenu = { onContextMenu } >
251304 { isGlobal ? (
252305 < VariableIcon className = { tw `size-6 text-slate-500` } />
@@ -304,7 +357,7 @@ const EnvironmentPanel = ({ environment: { environmentId, isGlobal, name }, id }
304357 >
305358 < VariablesTable environmentId = { environmentId } />
306359 </ Suspense >
307- </ TabPanel >
360+ </ div >
308361 ) ;
309362} ;
310363
0 commit comments