Skip to content

Commit a3bfd12

Browse files
Merge pull request #388 from LIT-Protocol/feat/ability-version-selection
chore(dashboard): bug fixes, update ability version selections
2 parents e49f1f6 + 35ed64a commit a3bfd12

18 files changed

+1039
-216
lines changed

packages/apps/app-dashboard/src/components/developer-dashboard/AbilitySelectorModal.tsx

Lines changed: 228 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { useState } from 'react';
12
import { AgGridReact } from 'ag-grid-react';
23
import {
34
ColDef,
45
ICellRendererParams,
56
ModuleRegistry,
67
AllCommunityModule,
78
RowClickedEvent,
9+
RowClassParams,
810
} from 'ag-grid-community';
11+
import { X } from 'lucide-react';
912

1013
import { Ability } from '@/types/developer-dashboard/appTypes';
1114
import {
@@ -17,22 +20,68 @@ import {
1720
} from '@/components/shared/ui/dialog';
1821
import { Button } from '@/components/shared/ui/button';
1922
import { theme, fonts } from '@/components/user-dashboard/connect/ui/theme';
23+
import { AbilityVersionSelectorModal } from './AbilityVersionSelectorModal';
2024

2125
// Register AG Grid modules
2226
ModuleRegistry.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

Comments
 (0)