Skip to content

Commit 1bbd628

Browse files
committed
test: add comprehensive tests for TagMultiSelect component
1 parent 19b91de commit 1bbd628

9 files changed

Lines changed: 808 additions & 141 deletions

File tree

frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {
2323
SelectValue,
2424
} from '@/components/ui/select';
2525
import { AddTaskDialogProps } from '@/components/utils/types';
26-
import { format } from 'date-fns';
2726
import { TagMultiSelect } from './TagMultiSelect';
27+
import { format } from 'date-fns';
2828

2929
export const AddTaskdialog = ({
3030
isOpen,

frontend/src/components/HomeComponents/Tasks/TagMultiSelect.tsx

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge';
33
import { Button } from '@/components/ui/button';
44
import { Input } from '@/components/ui/input';
55
import { TagMultiSelectProps } from '@/components/utils/types';
6-
import { ChevronDown, Plus } from 'lucide-react';
6+
import { ChevronDown, Plus, Check, X } from 'lucide-react';
77

88
export const TagMultiSelect = ({
99
availableTags,
@@ -12,8 +12,12 @@ export const TagMultiSelect = ({
1212
placeholder = 'Select or create tags',
1313
disabled = false,
1414
className = '',
15+
autoOpen = false,
16+
showActions = false,
17+
onSave,
18+
onCancel,
1519
}: TagMultiSelectProps) => {
16-
const [isOpen, setIsOpen] = useState(false);
20+
const [isOpen, setIsOpen] = useState(autoOpen);
1721
const [searchTerm, setSearchTerm] = useState('');
1822
const dropdownRef = useRef<HTMLDivElement>(null);
1923
const inputRef = useRef<HTMLInputElement>(null);
@@ -67,12 +71,15 @@ export const TagMultiSelect = ({
6771
const handleKeyDown = (e: React.KeyboardEvent) => {
6872
if (e.key === 'Enter') {
6973
e.preventDefault();
70-
const filteredTags = getFilteredTags();
71-
if (filteredTags.length > 0) {
72-
handleTagSelect(filteredTags[0]);
73-
} else if (searchTerm.trim()) {
74-
handleNewTagCreate();
74+
if (searchTerm.trim()) {
75+
const filteredTags = getFilteredTags();
76+
if (filteredTags.length > 0) {
77+
handleTagSelect(filteredTags[0]);
78+
} else {
79+
handleNewTagCreate();
80+
}
7581
}
82+
// Do nothing if search is empty
7683
} else if (e.key === 'Escape') {
7784
setIsOpen(false);
7885
setSearchTerm('');
@@ -92,6 +99,7 @@ export const TagMultiSelect = ({
9299
onClick={() => setIsOpen(!isOpen)}
93100
disabled={disabled}
94101
className="w-full justify-between text-left font-normal"
102+
aria-label="Select tags"
95103
>
96104
<span className="truncate">
97105
{selectedTags.length > 0
@@ -101,6 +109,50 @@ export const TagMultiSelect = ({
101109
<ChevronDown className="h-4 w-4 opacity-50" />
102110
</Button>
103111

112+
{/* Selected tags displayed below button with optional action buttons */}
113+
{selectedTags.length > 0 && (
114+
<div className="flex flex-wrap items-center gap-2 mt-2">
115+
{selectedTags.map((tag) => (
116+
<Badge key={tag} className="flex items-center gap-1">
117+
<span>{tag}</span>
118+
<button
119+
type="button"
120+
onClick={() => handleTagRemove(tag)}
121+
className="ml-1 text-red-500 hover:text-red-700 text-xs"
122+
disabled={disabled}
123+
aria-label={`Remove ${tag}`}
124+
>
125+
126+
</button>
127+
</Badge>
128+
))}
129+
{showActions && onSave && onCancel && (
130+
<div className="flex items-center gap-1 whitespace-nowrap">
131+
<Button
132+
type="button"
133+
variant="ghost"
134+
size="icon"
135+
onClick={onSave}
136+
className="h-8 w-8"
137+
aria-label="Save tags"
138+
>
139+
<Check className="h-4 w-4 text-green-500" />
140+
</Button>
141+
<Button
142+
type="button"
143+
variant="ghost"
144+
size="icon"
145+
onClick={onCancel}
146+
className="h-8 w-8"
147+
aria-label="Cancel"
148+
>
149+
<X className="h-4 w-4 text-red-500" />
150+
</Button>
151+
</div>
152+
)}
153+
</div>
154+
)}
155+
104156
{isOpen && (
105157
<div className="absolute z-50 w-full mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-hidden">
106158
<div className="p-2 border-b">
@@ -119,16 +171,10 @@ export const TagMultiSelect = ({
119171
{getFilteredTags().map((tag) => (
120172
<div
121173
key={tag}
122-
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
174+
className="px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
123175
onClick={() => handleTagSelect(tag)}
124176
>
125-
<input
126-
type="checkbox"
127-
checked={false}
128-
readOnly
129-
className="h-4 w-4"
130-
/>
131-
<span className="flex-1 text-sm">{tag}</span>
177+
<span className="text-sm">{tag}</span>
132178
</div>
133179
))}
134180

@@ -152,28 +198,6 @@ export const TagMultiSelect = ({
152198
</div>
153199
</div>
154200
)}
155-
156-
{selectedTags.length > 0 && (
157-
<div className="flex flex-wrap gap-2 mt-2">
158-
{selectedTags.map((tag) => (
159-
<Badge
160-
key={tag}
161-
variant="secondary"
162-
className="flex items-center gap-1"
163-
>
164-
<span>{tag}</span>
165-
<button
166-
type="button"
167-
onClick={() => handleTagRemove(tag)}
168-
className="ml-1 text-red-500 hover:text-red-700 text-xs"
169-
disabled={disabled}
170-
>
171-
172-
</button>
173-
</Badge>
174-
))}
175-
</div>
176-
)}
177201
</div>
178202
);
179203
};

frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,42 +1190,25 @@ export const TaskDialog = ({
11901190
<TableCell>Tags:</TableCell>
11911191
<TableCell>
11921192
{editState.isEditingTags ? (
1193-
<div className="space-y-2">
1194-
<TagMultiSelect
1195-
availableTags={uniqueTags}
1196-
selectedTags={editState.editedTags}
1197-
onTagsChange={(tags) =>
1198-
onUpdateState({ editedTags: tags })
1199-
}
1200-
placeholder="Select or create tags"
1201-
/>
1202-
<div className="flex items-center gap-2 whitespace-nowrap">
1203-
<Button
1204-
variant="ghost"
1205-
size="icon"
1206-
aria-label="save"
1207-
onClick={() => {
1208-
onSaveTags(task, editState.editedTags);
1209-
onUpdateState({ isEditingTags: false });
1210-
}}
1211-
>
1212-
<CheckIcon className="h-4 w-4 text-green-500" />
1213-
</Button>
1214-
<Button
1215-
variant="ghost"
1216-
size="icon"
1217-
aria-label="cancel"
1218-
onClick={() =>
1219-
onUpdateState({
1220-
isEditingTags: false,
1221-
editedTags: task.tags || [],
1222-
})
1223-
}
1224-
>
1225-
<XIcon className="h-4 w-4 text-red-500" />
1226-
</Button>
1227-
</div>
1228-
</div>
1193+
<TagMultiSelect
1194+
availableTags={uniqueTags}
1195+
selectedTags={editState.editedTags}
1196+
onTagsChange={(tags) =>
1197+
onUpdateState({ editedTags: tags })
1198+
}
1199+
placeholder="Select or create tags"
1200+
showActions={true}
1201+
onSave={() => {
1202+
onSaveTags(task, editState.editedTags);
1203+
onUpdateState({ isEditingTags: false });
1204+
}}
1205+
onCancel={() =>
1206+
onUpdateState({
1207+
isEditingTags: false,
1208+
editedTags: task.tags || [],
1209+
})
1210+
}
1211+
/>
12291212
) : (
12301213
<div className="flex items-center flex-wrap">
12311214
{task.tags !== null && task.tags.length >= 1 ? (

frontend/src/components/HomeComponents/Tasks/Tasks.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ export const Tasks = (
9696
const [isAddTaskOpen, setIsAddTaskOpen] = useState(false);
9797
const [_isDialogOpen, setIsDialogOpen] = useState(false);
9898
const [_selectedTask, setSelectedTask] = useState<Task | null>(null);
99-
const [editedTags, setEditedTags] = useState<string[]>(
100-
_selectedTask?.tags || []
101-
);
10299
const [searchTerm, setSearchTerm] = useState('');
103100
const [debouncedTerm, setDebouncedTerm] = useState('');
104101
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null);
@@ -188,7 +185,7 @@ export const Tasks = (
188185
}, [props.email]);
189186
useEffect(() => {
190187
if (_selectedTask) {
191-
setEditedTags(_selectedTask.tags || []);
188+
// Task selection effect - no longer needed for editedTags
192189
}
193190
}, [_selectedTask]);
194191

@@ -273,6 +270,12 @@ export const Tasks = (
273270
.filter((project) => project !== '')
274271
.sort((a, b) => (a > b ? 1 : -1));
275272
setUniqueProjects(filteredProjects);
273+
274+
const tagsSet = new Set(sortedTasks.flatMap((task) => task.tags || []));
275+
const filteredTags = Array.from(tagsSet)
276+
.filter((tag) => tag !== '')
277+
.sort((a, b) => (a > b ? 1 : -1));
278+
setUniqueTags(filteredTags);
276279
});
277280

278281
const currentTime = Date.now();
@@ -799,11 +802,7 @@ export const Tasks = (
799802
}, [selectedProjects, selectedTags, selectedStatuses, tasks, debouncedTerm]);
800803

801804
const handleSaveTags = (task: Task, tags: string[]) => {
802-
const currentTags = tags || [];
803-
const removedTags = currentTags.filter((tag) => !editedTags.includes(tag));
804-
const updatedTags = editedTags.filter((tag) => tag.trim() !== '');
805-
const tagsToRemove = removedTags.map((tag) => `${tag}`);
806-
const finalTags = [...updatedTags, ...tagsToRemove];
805+
const updatedTags = tags.filter((tag) => tag.trim() !== '');
807806

808807
setUnsyncedTaskUuids((prev) => new Set([...prev, task.uuid]));
809808

@@ -812,7 +811,7 @@ export const Tasks = (
812811
props.encryptionSecret,
813812
props.UUID,
814813
task.description,
815-
finalTags,
814+
updatedTags,
816815
task.uuid.toString(),
817816
task.project,
818817
task.start,

0 commit comments

Comments
 (0)