1+ import { useState } from 'react' ;
12import { AgGridReact } from 'ag-grid-react' ;
23import {
34 ColDef ,
45 ICellRendererParams ,
56 ModuleRegistry ,
67 AllCommunityModule ,
78 RowClickedEvent ,
9+ RowClassParams ,
810} from 'ag-grid-community' ;
11+ import { X } from 'lucide-react' ;
912
1013import { Ability } from '@/types/developer-dashboard/appTypes' ;
1114import {
@@ -17,22 +20,68 @@ import {
1720} from '@/components/shared/ui/dialog' ;
1821import { Button } from '@/components/shared/ui/button' ;
1922import { theme , fonts } from '@/components/user-dashboard/connect/ui/theme' ;
23+ import { AbilityVersionSelectorModal } from './AbilityVersionSelectorModal' ;
2024
2125// Register AG Grid modules
2226ModuleRegistry . registerModules ( [ AllCommunityModule ] ) ;
2327
24- // Static column definitions
25- const TOOL_GRID_COLUMNS : ColDef [ ] = [
28+ interface SelectedAbilityWithVersion {
29+ ability : Ability ;
30+ version : string ;
31+ }
32+
33+ // Dynamic column definitions based on selected abilities
34+ const createToolGridColumns = (
35+ selectedAbilities : Map < string , SelectedAbilityWithVersion > ,
36+ onAddClick : ( ability : Ability ) => void ,
37+ onRemoveClick : ( packageName : string ) => void ,
38+ ) : ColDef [ ] => [
39+ {
40+ headerName : '' ,
41+ field : 'packageName' ,
42+ width : 80 ,
43+ maxWidth : 80 ,
44+ minWidth : 80 ,
45+ suppressNavigable : true ,
46+ cellRenderer : ( params : ICellRendererParams ) => {
47+ const isSelected = selectedAbilities . has ( params . value ) ;
48+ return (
49+ < div className = "flex items-center justify-center h-full" >
50+ < button
51+ onClick = { ( e ) => {
52+ e . stopPropagation ( ) ;
53+ if ( isSelected ) {
54+ onRemoveClick ( params . value ) ;
55+ } else {
56+ onAddClick ( params . data ) ;
57+ }
58+ } }
59+ className = { `px-3 py-1 text-xs font-semibold rounded transition-colors ${
60+ isSelected
61+ ? 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 hover:bg-gray-400 dark:hover:bg-gray-500'
62+ : 'text-white hover:opacity-90'
63+ } `}
64+ style = { ! isSelected ? { backgroundColor : theme . brandOrange } : undefined }
65+ >
66+ { isSelected ? 'Added' : 'Add' }
67+ </ button >
68+ </ div >
69+ ) ;
70+ } ,
71+ } ,
2672 {
2773 headerName : 'Ability Name' ,
2874 field : 'title' ,
2975 flex : 2 ,
3076 minWidth : 200 ,
3177 cellRenderer : ( params : ICellRendererParams ) => {
78+ const isSelected = selectedAbilities . has ( params . data . packageName ) ;
3279 return (
3380 < div className = "flex items-center justify-between h-full" >
3481 < div >
35- < div className = "font-medium" > { params . value || params . data . packageName } </ div >
82+ < div className = { `font-medium ${ isSelected ? 'font-semibold' : '' } ` } >
83+ { params . value || params . data . packageName }
84+ </ div >
3685 </ div >
3786 </ div >
3887 ) ;
@@ -74,11 +123,21 @@ const TOOL_GRID_COLUMNS: ColDef[] = [
74123 headerName : 'Version' ,
75124 field : 'activeVersion' ,
76125 flex : 1 ,
77- minWidth : 100 ,
126+ minWidth : 120 ,
78127 cellRenderer : ( params : ICellRendererParams ) => {
128+ const selected = selectedAbilities . get ( params . data . packageName ) ;
129+ const displayVersion = selected ? selected . version : params . value ;
130+ const isSelected = ! ! selected ;
79131 return (
80132 < div className = "flex items-center h-full" >
81- < span > { params . value } </ span >
133+ < span className = { isSelected ? 'font-semibold' : '' } >
134+ { displayVersion }
135+ { isSelected && (
136+ < span className = "ml-2 text-xs" style = { { color : theme . brandOrange } } >
137+ (selected)
138+ </ span >
139+ ) }
140+ </ span >
82141 </ div >
83142 ) ;
84143 } ,
@@ -123,28 +182,102 @@ export function AbilitySelectorModal({
123182 existingAbilities,
124183 availableAbilities,
125184} : AbilitySelectorModalProps ) {
126- // Filter out already added abilities
185+ const [ selectedAbility , setSelectedAbility ] = useState < Ability | null > ( null ) ;
186+ const [ isVersionSelectorOpen , setIsVersionSelectorOpen ] = useState ( false ) ;
187+ const [ selectedAbilities , setSelectedAbilities ] = useState <
188+ Map < string , SelectedAbilityWithVersion >
189+ > ( new Map ( ) ) ;
190+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
191+
192+ // Filter out already added abilities and deleted abilities
127193 const filteredAbilities = availableAbilities . filter (
128- ( ability ) => ! existingAbilities . includes ( ability . packageName ) ,
194+ ( ability ) => ! existingAbilities . includes ( ability . packageName ) && ! ability . isDeleted ,
129195 ) ;
130196
197+ const handleAddClick = ( ability : Ability ) => {
198+ // Open version selector modal to select
199+ setSelectedAbility ( ability ) ;
200+ setIsVersionSelectorOpen ( true ) ;
201+ } ;
202+
131203 const handleRowClick = async ( event : RowClickedEvent ) => {
132204 const ability = event . data ;
133205 if ( ! ability ) {
134206 return ;
135207 }
136208
137- await onAbilityAdd ( ability ) ;
209+ // Only allow deselection via row click
210+ if ( selectedAbilities . has ( ability . packageName ) ) {
211+ handleRemoveSelection ( ability . packageName ) ;
212+ }
213+ } ;
214+
215+ const handleVersionSelect = async ( version : string ) => {
216+ if ( ! selectedAbility ) return ;
217+
218+ // Add to selected abilities map
219+ setSelectedAbilities ( ( prev ) => {
220+ const newMap = new Map ( prev ) ;
221+ newMap . set ( selectedAbility . packageName , {
222+ ability : selectedAbility ,
223+ version : version ,
224+ } ) ;
225+ return newMap ;
226+ } ) ;
227+
228+ // Close version selector modal but keep ability selector open
229+ setIsVersionSelectorOpen ( false ) ;
230+ setSelectedAbility ( null ) ;
231+ } ;
232+
233+ const handleRemoveSelection = ( packageName : string ) => {
234+ setSelectedAbilities ( ( prev ) => {
235+ const newMap = new Map ( prev ) ;
236+ newMap . delete ( packageName ) ;
237+ return newMap ;
238+ } ) ;
239+ } ;
240+
241+ const handleAddAbilities = async ( ) => {
242+ if ( selectedAbilities . size === 0 ) return ;
243+
244+ setIsSubmitting ( true ) ;
245+ try {
246+ // Add all selected abilities in parallel
247+ await Promise . all (
248+ Array . from ( selectedAbilities . values ( ) ) . map ( ( { ability, version } ) => {
249+ const abilityWithVersion = {
250+ ...ability ,
251+ activeVersion : version ,
252+ } ;
253+ return onAbilityAdd ( abilityWithVersion ) ;
254+ } ) ,
255+ ) ;
256+
257+ // Clear selections but keep modal open for more selections
258+ setSelectedAbilities ( new Map ( ) ) ;
259+ } catch ( error ) {
260+ console . error ( 'Failed to add abilities:' , error ) ;
261+ } finally {
262+ setIsSubmitting ( false ) ;
263+ }
264+ } ;
265+
266+ const handleVersionSelectorClose = ( ) => {
267+ setIsVersionSelectorOpen ( false ) ;
268+ setSelectedAbility ( null ) ;
138269 } ;
139270
140271 const handleOpenChange = ( open : boolean ) => {
141272 if ( ! open ) {
273+ setSelectedAbilities ( new Map ( ) ) ;
142274 onClose ( ) ;
143275 }
144276 } ;
145277
146- const getRowClass = ( ) => {
147- return 'cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-700' ;
278+ const getRowClass = ( params : RowClassParams ) => {
279+ const isSelected = selectedAbilities . has ( params . data ?. packageName ) ;
280+ return `cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-700 ${ isSelected ? 'bg-orange-50 dark:bg-orange-900/10' : '' } ` ;
148281 } ;
149282
150283 return (
@@ -158,9 +291,8 @@ export function AbilitySelectorModal({
158291 Add Abilities to App Version
159292 </ DialogTitle >
160293 < 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)` }
294+ Click "Add" to select a version for an ability. Click "Added" to deselect. You can
295+ select multiple abilities before adding them.
164296 </ DialogDescription >
165297 </ DialogHeader >
166298
@@ -182,7 +314,11 @@ export function AbilitySelectorModal({
182314 >
183315 < AgGridReact
184316 rowData = { filteredAbilities }
185- columnDefs = { TOOL_GRID_COLUMNS }
317+ columnDefs = { createToolGridColumns (
318+ selectedAbilities ,
319+ handleAddClick ,
320+ handleRemoveSelection ,
321+ ) }
186322 defaultColDef = { DEFAULT_COL_DEF }
187323 onRowClicked = { handleRowClick }
188324 getRowClass = { getRowClass }
@@ -195,12 +331,86 @@ export function AbilitySelectorModal({
195331 </ div >
196332 </ div >
197333
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 >
334+ { /* Selected Abilities List */ }
335+ { selectedAbilities . size > 0 && (
336+ < div className = { `flex-shrink-0 border ${ theme . mainCardBorder } rounded-lg p-4` } >
337+ < div className = "flex items-center justify-between mb-2" >
338+ < h4 className = { `text-sm font-semibold ${ theme . text } ` } style = { fonts . heading } >
339+ Selected Abilities ({ selectedAbilities . size } )
340+ </ h4 >
341+ </ div >
342+ < div className = "space-y-2 max-h-32 overflow-y-auto" >
343+ { Array . from ( selectedAbilities . values ( ) ) . map ( ( { ability, version } ) => (
344+ < div
345+ key = { ability . packageName }
346+ className = { `flex items-center justify-between px-3 py-2 rounded-lg ${ theme . itemBg } ` }
347+ >
348+ < div className = "flex-1 min-w-0" >
349+ < div className = "flex items-center gap-2" >
350+ < span className = { `text-sm font-medium ${ theme . text } truncate` } >
351+ { ability . title || ability . packageName }
352+ </ span >
353+ < span
354+ className = "text-xs px-2 py-0.5 rounded"
355+ style = { { backgroundColor : theme . brandOrange , color : 'white' } }
356+ >
357+ v{ version }
358+ </ span >
359+ </ div >
360+ < div className = { `text-xs ${ theme . textMuted } font-mono truncate` } >
361+ { ability . packageName }
362+ </ div >
363+ </ div >
364+ < button
365+ onClick = { ( ) => handleRemoveSelection ( ability . packageName ) }
366+ className = { `ml-2 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30` }
367+ title = "Remove from selection"
368+ >
369+ < X className = "w-4 h-4 text-red-600 dark:text-red-400" />
370+ </ button >
371+ </ div >
372+ ) ) }
373+ </ div >
374+ </ div >
375+ ) }
376+
377+ < div
378+ className = { `flex justify-between items-center gap-2 pt-4 border-t ${ theme . cardBorder } flex-shrink-0` }
379+ >
380+ < div className = { `text-sm ${ theme . textMuted } ` } >
381+ { selectedAbilities . size > 0 ? (
382+ < span >
383+ { selectedAbilities . size } { selectedAbilities . size === 1 ? 'ability' : 'abilities' } { ' ' }
384+ selected
385+ </ span >
386+ ) : (
387+ < span > Select abilities to add to your app version</ span >
388+ ) }
389+ </ div >
390+ < div className = "flex gap-2" >
391+ < Button variant = "outline" onClick = { onClose } disabled = { isSubmitting } >
392+ Cancel
393+ </ Button >
394+ < Button
395+ onClick = { handleAddAbilities }
396+ disabled = { selectedAbilities . size === 0 || isSubmitting }
397+ style = { { backgroundColor : theme . brandOrange , ...fonts . body } }
398+ >
399+ { isSubmitting
400+ ? 'Adding Abilities...'
401+ : `Add ${ selectedAbilities . size > 0 ? selectedAbilities . size : '' } ${ selectedAbilities . size === 1 ? 'Ability' : 'Abilities' } ` }
402+ </ Button >
403+ </ div >
202404 </ div >
203405 </ DialogContent >
406+
407+ { /* Version Selector Modal */ }
408+ < AbilityVersionSelectorModal
409+ isOpen = { isVersionSelectorOpen }
410+ onClose = { handleVersionSelectorClose }
411+ onVersionSelect = { handleVersionSelect }
412+ ability = { selectedAbility }
413+ />
204414 </ Dialog >
205415 ) ;
206416}
0 commit comments