Skip to content

Commit c91cc49

Browse files
committed
feat: multi package selection
1 parent 85274c8 commit c91cc49

File tree

2 files changed

+156
-52
lines changed

2 files changed

+156
-52
lines changed

src/ui/App.tsx

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@ import { ErrorMessage } from './ErrorMessage.js';
55
import { PackageItem } from './PackageItem.js';
66
import { Package, ReleaseType } from './types.js';
77

8+
type SubmitButtonProps = {
9+
isSubmitting: boolean;
10+
selections: Record<string, string>;
11+
packageDependencyErrors: Record<
12+
string,
13+
{ missingDependentNames: string[]; missingDependencies: string[] }
14+
>;
15+
onSubmit: () => Promise<void>;
16+
};
17+
18+
function SubmitButton({
19+
isSubmitting,
20+
selections,
21+
packageDependencyErrors,
22+
onSubmit,
23+
}: SubmitButtonProps) {
24+
const isDisabled =
25+
isSubmitting ||
26+
Object.keys(selections).length === 0 ||
27+
Object.keys(packageDependencyErrors).length > 0 ||
28+
Object.values(selections).every((value) => value === 'intentionally-skip');
29+
30+
return (
31+
<button
32+
onClick={() => void onSubmit()}
33+
disabled={isDisabled}
34+
className={`mt-6 px-4 py-2 rounded ${
35+
isDisabled
36+
? 'bg-blue-300 cursor-not-allowed'
37+
: 'bg-blue-500 hover:bg-blue-600'
38+
} text-white`}
39+
>
40+
{isSubmitting
41+
? 'Processing Release (This may take a few minutes)...'
42+
: 'Submit Release Selections'}
43+
</button>
44+
);
45+
}
46+
847
function App() {
948
const [packages, setPackages] = useState<Package[]>([]);
1049
const [selections, setSelections] = useState<Record<string, string>>({});
@@ -28,6 +67,9 @@ function App() {
2867
>
2968
>({});
3069
const [isSuccess, setIsSuccess] = useState(false);
70+
const [selectedPackages, setSelectedPackages] = useState<Set<string>>(
71+
new Set(),
72+
);
3173

3274
useEffect(() => {
3375
fetch('/api/packages')
@@ -212,6 +254,42 @@ function App() {
212254
}
213255
};
214256

257+
const handleBulkAction = (action: ReleaseType) => {
258+
const newSelections = { ...selections };
259+
selectedPackages.forEach((packageName) => {
260+
newSelections[packageName] = action;
261+
});
262+
setSelections(newSelections);
263+
setSelectedPackages(new Set());
264+
};
265+
266+
const togglePackageSelection = (packageName: string) => {
267+
setSelectedPackages((prev) => {
268+
const newSet = new Set(prev);
269+
if (newSet.has(packageName)) {
270+
newSet.delete(packageName);
271+
} else {
272+
newSet.add(packageName);
273+
}
274+
return newSet;
275+
});
276+
};
277+
278+
if (isSubmitting) {
279+
return (
280+
<div className="fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
281+
<div className="text-center p-8">
282+
<h2 className="text-2xl font-bold mb-4">Processing Release</h2>
283+
<p className="mb-6">
284+
Please wait while we process your release. This may take a few
285+
minutes...
286+
</p>
287+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
288+
</div>
289+
</div>
290+
);
291+
}
292+
215293
if (isSuccess) {
216294
return (
217295
<div className="fixed inset-0 bg-white flex items-center justify-center">
@@ -235,6 +313,23 @@ function App() {
235313
Create Release Branch Interactive UI
236314
</h1>
237315

316+
{selectedPackages.size > 0 && (
317+
<div className="mb-4 p-4 bg-gray-100 rounded">
318+
<span className="mr-2">
319+
Bulk action for {selectedPackages.size} packages:
320+
</span>
321+
{['major', 'minor', 'patch', 'intentionally-skip'].map((action) => (
322+
<button
323+
key={action}
324+
onClick={() => handleBulkAction(action as ReleaseType)}
325+
className="mr-2 px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
326+
>
327+
{action}
328+
</button>
329+
))}
330+
</div>
331+
)}
332+
238333
<div className="space-y-4">
239334
{packages.map((pkg) => (
240335
<PackageItem
@@ -250,28 +345,19 @@ function App() {
250345
onFetchChangelog={fetchChangelog}
251346
setSelections={setSelections}
252347
setChangelogs={setChangelogs}
348+
isSelected={selectedPackages.has(pkg.name)}
349+
onToggleSelect={() => togglePackageSelection(pkg.name)}
253350
/>
254351
))}
255352
</div>
256353

257354
{packages.length > 0 && (
258-
<button
259-
onClick={() => void handleSubmit()}
260-
disabled={
261-
isSubmitting ||
262-
Object.keys(selections).length === 0 ||
263-
Object.keys(packageDependencyErrors).length > 0
264-
}
265-
className={`mt-6 px-4 py-2 rounded ${
266-
isSubmitting ||
267-
Object.keys(selections).length === 0 ||
268-
Object.keys(packageDependencyErrors).length > 0
269-
? 'bg-blue-300 cursor-not-allowed'
270-
: 'bg-blue-500 hover:bg-blue-600'
271-
} text-white`}
272-
>
273-
{isSubmitting ? 'Submitting...' : 'Submit Release Selections'}
274-
</button>
355+
<SubmitButton
356+
isSubmitting={isSubmitting}
357+
selections={selections}
358+
packageDependencyErrors={packageDependencyErrors}
359+
onSubmit={handleSubmit}
360+
/>
275361
)}
276362

277363
{error && <div className="text-red-600 p-4">Error: {error}</div>}

src/ui/PackageItem.tsx

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type PackageItemProps = {
2222
onFetchChangelog: (packageName: string) => Promise<void>;
2323
setSelections: React.Dispatch<React.SetStateAction<Record<string, string>>>;
2424
setChangelogs: React.Dispatch<React.SetStateAction<Record<string, string>>>;
25+
isSelected: boolean;
26+
onToggleSelect: () => void;
2527
};
2628

2729
export function PackageItem({
@@ -36,6 +38,8 @@ export function PackageItem({
3638
onFetchChangelog,
3739
setSelections,
3840
setChangelogs,
41+
isSelected,
42+
onToggleSelect,
3943
}: PackageItemProps) {
4044
return (
4145
<div
@@ -52,42 +56,56 @@ export function PackageItem({
5256
: ''
5357
}`}
5458
>
55-
<h2 className="text-xl font-semibold">{pkg.name}</h2>
56-
<div className="flex items-center justify-between">
57-
<div>
58-
<p className="text-gray-600">Current version: {pkg.version}</p>
59-
{selections[pkg.name] &&
60-
selections[pkg.name] !== 'intentionally-skip' &&
61-
selections[pkg.name] !== 'custom' &&
62-
!versionErrors[pkg.name] && (
63-
<p className="text-yellow-700">
64-
New version:{' '}
65-
{!['patch', 'minor', 'major'].includes(selections[pkg.name])
66-
? selections[pkg.name]
67-
: new SemVer(pkg.version)
68-
.inc(
69-
selections[pkg.name] as Exclude<
70-
ReleaseType,
71-
'intentionally-skip' | 'custom' | string
72-
>,
73-
)
74-
.toString()}
75-
</p>
76-
)}
77-
{versionErrors[pkg.name] && (
78-
<p className="text-red-500 text-sm mt-1">
79-
{versionErrors[pkg.name]}
80-
</p>
81-
)}
59+
<div className="flex items-start gap-3">
60+
<div className="pt-1">
61+
<input
62+
type="checkbox"
63+
checked={isSelected}
64+
onChange={onToggleSelect}
65+
className={`h-5 w-5 rounded border-gray-300
66+
${isSelected ? 'text-blue-600' : 'text-gray-300'}
67+
`}
68+
/>
69+
</div>
70+
<div className="flex-1">
71+
<h2 className="text-xl font-semibold">{pkg.name}</h2>
72+
<div className="flex items-center justify-between">
73+
<div>
74+
<p className="text-gray-600">Current version: {pkg.version}</p>
75+
{selections[pkg.name] &&
76+
selections[pkg.name] !== 'intentionally-skip' &&
77+
selections[pkg.name] !== 'custom' &&
78+
!versionErrors[pkg.name] && (
79+
<p className="text-yellow-700">
80+
New version:{' '}
81+
{!['patch', 'minor', 'major'].includes(selections[pkg.name])
82+
? selections[pkg.name]
83+
: new SemVer(pkg.version)
84+
.inc(
85+
selections[pkg.name] as Exclude<
86+
ReleaseType,
87+
'intentionally-skip' | 'custom' | string
88+
>,
89+
)
90+
.toString()}
91+
</p>
92+
)}
93+
{versionErrors[pkg.name] && (
94+
<p className="text-red-500 text-sm mt-1">
95+
{versionErrors[pkg.name]}
96+
</p>
97+
)}
98+
</div>
99+
<VersionSelector
100+
packageName={pkg.name}
101+
selection={selections[pkg.name]}
102+
onSelectionChange={onSelectionChange}
103+
onCustomVersionChange={onCustomVersionChange}
104+
onFetchChangelog={onFetchChangelog}
105+
isLoadingChangelog={loadingChangelogs[pkg.name] === true}
106+
/>
107+
</div>
82108
</div>
83-
<VersionSelector
84-
packageName={pkg.name}
85-
selection={selections[pkg.name]}
86-
onSelectionChange={onSelectionChange}
87-
onCustomVersionChange={onCustomVersionChange}
88-
onFetchChangelog={onFetchChangelog}
89-
isLoadingChangelog={loadingChangelogs[pkg.name] === true}
90-
/>
91109
</div>
92110

93111
{packageDependencyErrors[pkg.name] && (

0 commit comments

Comments
 (0)