1+ import { useState } from 'react' ;
12import { AgGridReact } from 'ag-grid-react' ;
23import {
34 ColDef ,
67 AllCommunityModule ,
78 RowClickedEvent ,
89} from 'ag-grid-community' ;
10+ import { CheckCircle2 , X } from 'lucide-react' ;
911
1012import { Ability } from '@/types/developer-dashboard/appTypes' ;
1113import {
@@ -17,22 +19,49 @@ import {
1719} from '@/components/shared/ui/dialog' ;
1820import { Button } from '@/components/shared/ui/button' ;
1921import { theme , fonts } from '@/components/user-dashboard/connect/ui/theme' ;
22+ import { AbilityVersionSelectorModal } from './AbilityVersionSelectorModal' ;
2023
2124// Register AG Grid modules
2225ModuleRegistry . registerModules ( [ AllCommunityModule ] ) ;
2326
24- // Static column definitions
25- const TOOL_GRID_COLUMNS : ColDef [ ] = [
27+ interface SelectedAbilityWithVersion {
28+ ability : Ability ;
29+ version : string ;
30+ }
31+
32+ // Dynamic column definitions based on selected abilities
33+ const createToolGridColumns = (
34+ selectedAbilities : Map < string , SelectedAbilityWithVersion > ,
35+ ) : ColDef [ ] => [
36+ {
37+ headerName : '' ,
38+ field : 'packageName' ,
39+ width : 50 ,
40+ maxWidth : 50 ,
41+ minWidth : 50 ,
42+ suppressNavigable : true ,
43+ cellRenderer : ( params : ICellRendererParams ) => {
44+ const isSelected = selectedAbilities . has ( params . value ) ;
45+ return (
46+ < div className = "flex items-center justify-center h-full" >
47+ { isSelected && < CheckCircle2 className = "w-5 h-5" style = { { color : theme . brandOrange } } /> }
48+ </ div >
49+ ) ;
50+ } ,
51+ } ,
2652 {
2753 headerName : 'Ability Name' ,
2854 field : 'title' ,
2955 flex : 2 ,
3056 minWidth : 200 ,
3157 cellRenderer : ( params : ICellRendererParams ) => {
58+ const isSelected = selectedAbilities . has ( params . data . packageName ) ;
3259 return (
3360 < div className = "flex items-center justify-between h-full" >
3461 < div >
35- < div className = "font-medium" > { params . value || params . data . packageName } </ div >
62+ < div className = { `font-medium ${ isSelected ? 'font-semibold' : '' } ` } >
63+ { params . value || params . data . packageName }
64+ </ div >
3665 </ div >
3766 </ div >
3867 ) ;
@@ -74,11 +103,21 @@ const TOOL_GRID_COLUMNS: ColDef[] = [
74103 headerName : 'Version' ,
75104 field : 'activeVersion' ,
76105 flex : 1 ,
77- minWidth : 100 ,
106+ minWidth : 120 ,
78107 cellRenderer : ( params : ICellRendererParams ) => {
108+ const selected = selectedAbilities . get ( params . data . packageName ) ;
109+ const displayVersion = selected ? selected . version : params . value ;
110+ const isSelected = ! ! selected ;
79111 return (
80112 < div className = "flex items-center h-full" >
81- < span > { params . value } </ span >
113+ < span className = { isSelected ? 'font-semibold' : '' } >
114+ { displayVersion }
115+ { isSelected && (
116+ < span className = "ml-2 text-xs" style = { { color : theme . brandOrange } } >
117+ (selected)
118+ </ span >
119+ ) }
120+ </ span >
82121 </ div >
83122 ) ;
84123 } ,
@@ -123,9 +162,16 @@ export function AbilitySelectorModal({
123162 existingAbilities,
124163 availableAbilities,
125164} : AbilitySelectorModalProps ) {
126- // Filter out already added abilities
165+ const [ selectedAbility , setSelectedAbility ] = useState < Ability | null > ( null ) ;
166+ const [ isVersionSelectorOpen , setIsVersionSelectorOpen ] = useState ( false ) ;
167+ const [ selectedAbilities , setSelectedAbilities ] = useState <
168+ Map < string , SelectedAbilityWithVersion >
169+ > ( new Map ( ) ) ;
170+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
171+
172+ // Filter out already added abilities and deleted abilities
127173 const filteredAbilities = availableAbilities . filter (
128- ( ability ) => ! existingAbilities . includes ( ability . packageName ) ,
174+ ( ability ) => ! existingAbilities . includes ( ability . packageName ) && ! ability . isDeleted ,
129175 ) ;
130176
131177 const handleRowClick = async ( event : RowClickedEvent ) => {
@@ -134,17 +180,83 @@ export function AbilitySelectorModal({
134180 return ;
135181 }
136182
137- await onAbilityAdd ( ability ) ;
183+ // If already selected, deselect it
184+ if ( selectedAbilities . has ( ability . packageName ) ) {
185+ handleRemoveSelection ( ability . packageName ) ;
186+ return ;
187+ }
188+
189+ // Open version selector modal to select
190+ setSelectedAbility ( ability ) ;
191+ setIsVersionSelectorOpen ( true ) ;
192+ } ;
193+
194+ const handleVersionSelect = async ( version : string ) => {
195+ if ( ! selectedAbility ) return ;
196+
197+ // Add to selected abilities map
198+ setSelectedAbilities ( ( prev ) => {
199+ const newMap = new Map ( prev ) ;
200+ newMap . set ( selectedAbility . packageName , {
201+ ability : selectedAbility ,
202+ version : version ,
203+ } ) ;
204+ return newMap ;
205+ } ) ;
206+
207+ // Close version selector modal but keep ability selector open
208+ setIsVersionSelectorOpen ( false ) ;
209+ setSelectedAbility ( null ) ;
210+ } ;
211+
212+ const handleRemoveSelection = ( packageName : string ) => {
213+ setSelectedAbilities ( ( prev ) => {
214+ const newMap = new Map ( prev ) ;
215+ newMap . delete ( packageName ) ;
216+ return newMap ;
217+ } ) ;
218+ } ;
219+
220+ const handleAddAbilities = async ( ) => {
221+ if ( selectedAbilities . size === 0 ) return ;
222+
223+ setIsSubmitting ( true ) ;
224+ try {
225+ // Add all selected abilities in parallel
226+ await Promise . all (
227+ Array . from ( selectedAbilities . values ( ) ) . map ( ( { ability, version } ) => {
228+ const abilityWithVersion = {
229+ ...ability ,
230+ activeVersion : version ,
231+ } ;
232+ return onAbilityAdd ( abilityWithVersion ) ;
233+ } ) ,
234+ ) ;
235+
236+ // Clear selections but keep modal open for more selections
237+ setSelectedAbilities ( new Map ( ) ) ;
238+ } catch ( error ) {
239+ console . error ( 'Failed to add abilities:' , error ) ;
240+ } finally {
241+ setIsSubmitting ( false ) ;
242+ }
243+ } ;
244+
245+ const handleVersionSelectorClose = ( ) => {
246+ setIsVersionSelectorOpen ( false ) ;
247+ setSelectedAbility ( null ) ;
138248 } ;
139249
140250 const handleOpenChange = ( open : boolean ) => {
141251 if ( ! open ) {
252+ setSelectedAbilities ( new Map ( ) ) ;
142253 onClose ( ) ;
143254 }
144255 } ;
145256
146- const getRowClass = ( ) => {
147- return 'cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-700' ;
257+ const getRowClass = ( params : any ) => {
258+ const isSelected = selectedAbilities . has ( params . data ?. packageName ) ;
259+ return `cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-700 ${ isSelected ? 'bg-orange-50 dark:bg-orange-900/10' : '' } ` ;
148260 } ;
149261
150262 return (
@@ -158,9 +270,8 @@ export function AbilitySelectorModal({
158270 Add Abilities to App Version
159271 </ DialogTitle >
160272 < DialogDescription className = { `${ theme . textMuted } ` } style = { fonts . body } >
161- Click any ability to add it immediately to your app version.
162- { existingAbilities . length > 0 &&
163- ` (${ existingAbilities . length } abilities already added)` }
273+ Click any ability to select a version. Click again to deselect. You can select multiple
274+ abilities before adding them.
164275 </ DialogDescription >
165276 </ DialogHeader >
166277
@@ -182,7 +293,7 @@ export function AbilitySelectorModal({
182293 >
183294 < AgGridReact
184295 rowData = { filteredAbilities }
185- columnDefs = { TOOL_GRID_COLUMNS }
296+ columnDefs = { createToolGridColumns ( selectedAbilities ) }
186297 defaultColDef = { DEFAULT_COL_DEF }
187298 onRowClicked = { handleRowClick }
188299 getRowClass = { getRowClass }
@@ -195,12 +306,86 @@ export function AbilitySelectorModal({
195306 </ div >
196307 </ div >
197308
198- < div className = { `flex justify-end gap-2 pt-4 border-t ${ theme . cardBorder } flex-shrink-0` } >
199- < Button variant = "outline" onClick = { onClose } >
200- Close
201- </ Button >
309+ { /* Selected Abilities List */ }
310+ { selectedAbilities . size > 0 && (
311+ < div className = { `flex-shrink-0 border ${ theme . mainCardBorder } rounded-lg p-4` } >
312+ < div className = "flex items-center justify-between mb-2" >
313+ < h4 className = { `text-sm font-semibold ${ theme . text } ` } style = { fonts . heading } >
314+ Selected Abilities ({ selectedAbilities . size } )
315+ </ h4 >
316+ </ div >
317+ < div className = "space-y-2 max-h-32 overflow-y-auto" >
318+ { Array . from ( selectedAbilities . values ( ) ) . map ( ( { ability, version } ) => (
319+ < div
320+ key = { ability . packageName }
321+ className = { `flex items-center justify-between px-3 py-2 rounded-lg ${ theme . itemBg } ` }
322+ >
323+ < div className = "flex-1 min-w-0" >
324+ < div className = "flex items-center gap-2" >
325+ < span className = { `text-sm font-medium ${ theme . text } truncate` } >
326+ { ability . title || ability . packageName }
327+ </ span >
328+ < span
329+ className = "text-xs px-2 py-0.5 rounded"
330+ style = { { backgroundColor : theme . brandOrange , color : 'white' } }
331+ >
332+ v{ version }
333+ </ span >
334+ </ div >
335+ < div className = { `text-xs ${ theme . textMuted } font-mono truncate` } >
336+ { ability . packageName }
337+ </ div >
338+ </ div >
339+ < button
340+ onClick = { ( ) => handleRemoveSelection ( ability . packageName ) }
341+ className = { `ml-2 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30` }
342+ title = "Remove from selection"
343+ >
344+ < X className = "w-4 h-4 text-red-600 dark:text-red-400" />
345+ </ button >
346+ </ div >
347+ ) ) }
348+ </ div >
349+ </ div >
350+ ) }
351+
352+ < div
353+ className = { `flex justify-between items-center gap-2 pt-4 border-t ${ theme . cardBorder } flex-shrink-0` }
354+ >
355+ < div className = { `text-sm ${ theme . textMuted } ` } >
356+ { selectedAbilities . size > 0 ? (
357+ < span >
358+ { selectedAbilities . size } { selectedAbilities . size === 1 ? 'ability' : 'abilities' } { ' ' }
359+ selected
360+ </ span >
361+ ) : (
362+ < span > Select abilities to add to your app version</ span >
363+ ) }
364+ </ div >
365+ < div className = "flex gap-2" >
366+ < Button variant = "outline" onClick = { onClose } disabled = { isSubmitting } >
367+ Cancel
368+ </ Button >
369+ < Button
370+ onClick = { handleAddAbilities }
371+ disabled = { selectedAbilities . size === 0 || isSubmitting }
372+ style = { { backgroundColor : theme . brandOrange , ...fonts . body } }
373+ >
374+ { isSubmitting
375+ ? 'Adding Abilities...'
376+ : `Add ${ selectedAbilities . size > 0 ? selectedAbilities . size : '' } ${ selectedAbilities . size === 1 ? 'Ability' : 'Abilities' } ` }
377+ </ Button >
378+ </ div >
202379 </ div >
203380 </ DialogContent >
381+
382+ { /* Version Selector Modal */ }
383+ < AbilityVersionSelectorModal
384+ isOpen = { isVersionSelectorOpen }
385+ onClose = { handleVersionSelectorClose }
386+ onVersionSelect = { handleVersionSelect }
387+ ability = { selectedAbility }
388+ />
204389 </ Dialog >
205390 ) ;
206391}
0 commit comments