Skip to content

Commit 56a1b44

Browse files
committed
feat(evaluations): multi-select
1 parent 818445d commit 56a1b44

7 files changed

Lines changed: 311 additions & 39 deletions

File tree

project-evaluations/src/components/AddModal.tsx

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef } from "react";
1+
import { useState } from "react";
22

33
import type { Category } from "../types.js";
44
import { CATEGORIES } from "../schema.js";
@@ -9,24 +9,27 @@ interface AddModalProps {
99
}
1010

1111
export default function AddModal({ onClose }: AddModalProps) {
12-
const titleRef = useRef<HTMLInputElement>(null);
13-
const descRef = useRef<HTMLTextAreaElement>(null);
14-
const categoryRef = useRef<HTMLSelectElement>(null);
12+
const [title, setTitle] = useState("");
13+
const [description, setDescription] = useState("");
14+
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
1515

1616
const { addEvaluation } = useEvaluationsData();
1717

1818
const fieldClass =
1919
"w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-indigo-500";
2020

2121
const handleConfirm = (): void => {
22-
const title = titleRef.current?.value.trim() ?? "";
23-
if (!title) {
22+
const trimmedTitle = title.trim();
23+
if (!trimmedTitle) {
2424
alert("Please enter a title.");
2525
return;
2626
}
27-
const description = descRef.current?.value.trim() ?? "";
28-
const category = (categoryRef.current?.value ?? CATEGORIES[0]) as Category;
29-
addEvaluation(title, description, category);
27+
if (selectedCategories.length === 0) {
28+
alert("Please select at least one category.");
29+
return;
30+
}
31+
const trimmedDescription = description.trim();
32+
addEvaluation(trimmedTitle, trimmedDescription, selectedCategories);
3033
onClose();
3134
};
3235

@@ -41,21 +44,49 @@ export default function AddModal({ onClose }: AddModalProps) {
4144
<div className="space-y-3">
4245
<div>
4346
<label className="block text-xs text-gray-400 mb-1">Title</label>
44-
<input ref={titleRef} type="text" placeholder="Protocol name" className={fieldClass} />
47+
<input
48+
type="text"
49+
placeholder="Protocol name"
50+
value={title}
51+
onChange={(e) => {
52+
setTitle(e.target.value);
53+
}}
54+
className={fieldClass}
55+
/>
4556
</div>
4657
<div>
4758
<label className="block text-xs text-gray-400 mb-1">Description</label>
48-
<textarea ref={descRef} rows={3} placeholder="Brief description" className={`${fieldClass} resize-none`} />
59+
<textarea
60+
rows={3}
61+
placeholder="Brief
62+
description"
63+
value={description}
64+
onChange={(e) => {
65+
setDescription(e.target.value);
66+
}}
67+
className={`${fieldClass} resize-none`}
68+
/>
4969
</div>
5070
<div>
51-
<label className="block text-xs text-gray-400 mb-1">Category</label>
52-
<select ref={categoryRef} className={fieldClass}>
53-
{CATEGORIES.map((c) => (
54-
<option key={c} value={c}>
55-
{c}
56-
</option>
71+
<label className="block text-xs text-gray-400 mb-2">Categories</label>
72+
<div className="space-y-2 bg-gray-800 border border-gray-700 rounded p-3">
73+
{CATEGORIES.map((cat) => (
74+
<label key={cat} className="flex items-center gap-2 cursor-pointer">
75+
<input
76+
type="checkbox"
77+
checked={selectedCategories.includes(cat)}
78+
onChange={(e) => {
79+
const updated = e.target.checked
80+
? [...selectedCategories, cat]
81+
: selectedCategories.filter((c) => c !== cat);
82+
setSelectedCategories(updated);
83+
}}
84+
className="w-4 h-4 bg-gray-700 border border-gray-600 rounded focus:outline-none focus:border-indigo-500"
85+
/>
86+
<span className="text-xs text-gray-300">{cat}</span>
87+
</label>
5788
))}
58-
</select>
89+
</div>
5990
</div>
6091
</div>
6192
<div className="flex gap-3 mt-5">

project-evaluations/src/components/PropertyRow.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,43 @@ export default function PropertyRow({ def, prop, onUpdate }: PropertyRowProps) {
1515
const inputClass =
1616
"w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-100 focus:outline-none focus:border-indigo-500";
1717

18+
const parseMultiSelectValue = (): string[] => {
19+
if (!prop?.value) return [];
20+
try {
21+
return JSON.parse(prop.value) as string[];
22+
} catch {
23+
return [];
24+
}
25+
};
26+
27+
const handleMultiSelectChange = (option: string, checked: boolean): void => {
28+
const current = parseMultiSelectValue();
29+
const updated = checked ? [...current, option] : current.filter((v) => v !== option);
30+
onUpdate(def.name, "value", JSON.stringify(updated));
31+
};
32+
1833
const renderValueInput = (): React.ReactNode => {
34+
if (def.inputType === "multi-select" && def.options) {
35+
const selected = parseMultiSelectValue();
36+
return (
37+
<div className="space-y-2 bg-gray-800 border border-gray-700 rounded p-2">
38+
{def.options.map((opt) => (
39+
<label key={opt} className="flex items-center gap-2 cursor-pointer">
40+
<input
41+
type="checkbox"
42+
checked={selected.includes(opt)}
43+
onChange={(e) => {
44+
handleMultiSelectChange(opt, e.target.checked);
45+
}}
46+
className="w-4 h-4 bg-gray-700 border border-gray-600 rounded focus:outline-none focus:border-indigo-500"
47+
/>
48+
<span className="text-xs text-gray-300">{opt}</span>
49+
</label>
50+
))}
51+
</div>
52+
);
53+
}
54+
1955
if (def.inputType === "select" && def.options) {
2056
return (
2157
<select
@@ -34,6 +70,7 @@ export default function PropertyRow({ def, prop, onUpdate }: PropertyRowProps) {
3470
</select>
3571
);
3672
}
73+
3774
return (
3875
<input
3976
type={def.inputType === "number" ? "number" : "text"}
@@ -47,12 +84,21 @@ export default function PropertyRow({ def, prop, onUpdate }: PropertyRowProps) {
4784
);
4885
};
4986

87+
const getDisplayValue = (): string => {
88+
if (def.inputType === "multi-select") {
89+
const selected = parseMultiSelectValue();
90+
return selected.length > 0 ? selected.join(", ") : "— none selected —";
91+
}
92+
return prop?.value ?? "";
93+
};
94+
5095
return (
5196
<div className="bg-gray-900 border border-gray-800 rounded-lg p-3 space-y-2">
5297
<div className="flex items-start justify-between gap-2">
5398
<div>
5499
<p className="text-xs font-medium text-gray-200">{def.name}</p>
55100
<p className="text-xs text-gray-500 mt-0.5">{def.description}</p>
101+
{def.inputType === "multi-select" && <p className="text-xs text-indigo-400 mt-1">{getDisplayValue()}</p>}
56102
</div>
57103
</div>
58104
{renderValueInput()}

0 commit comments

Comments
 (0)