Skip to content

Commit 1ba860d

Browse files
committed
feat(tags): add tag selector dropdown with multi-select & autocomplete
Implemented new TagSelector component with searchable dropdown and create-tag support. Replaced manual tag input in Add Task and Edit Task dialogs. Integrated existing user-defined tags (uniqueTags) into the selector. Removed Popover.Portal to fix cursor and interaction issues inside dialogs. Updated tests to mock TagSelector and verify new tag state updates. Improves tagging UX and prevents inconsistent or typo-prone tags. Fixes: #210
1 parent 543a4a8 commit 1ba860d

File tree

4 files changed

+236
-248
lines changed

4 files changed

+236
-248
lines changed

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

Lines changed: 37 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
import Pagination from './Pagination';
6262
import { url } from '@/components/utils/URLs';
6363
import { MultiSelectFilter } from '@/components/ui/multi-select';
64+
import { TagSelector } from '@/components/ui/tagSelector';
6465
import BottomBar from '../BottomBar/BottomBar';
6566
import {
6667
addTaskToBackend,
@@ -106,15 +107,13 @@ export const Tasks = (
106107
});
107108
const [isAddTaskOpen, setIsAddTaskOpen] = useState(false);
108109
const [_isDialogOpen, setIsDialogOpen] = useState(false);
109-
const [tagInput, setTagInput] = useState('');
110110

111111
const [isEditing, setIsEditing] = useState(false);
112112
const [editedDescription, setEditedDescription] = useState('');
113113
const [_selectedTask, setSelectedTask] = useState<Task | null>(null);
114114
const [editedTags, setEditedTags] = useState<string[]>(
115115
_selectedTask?.tags || []
116116
);
117-
const [editTagInput, setEditTagInput] = useState<string>('');
118117
const [isEditingTags, setIsEditingTags] = useState(false);
119118
const [isEditingPriority, setIsEditingPriority] = useState(false);
120119
const [editedPriority, setEditedPriority] = useState('NONE');
@@ -609,22 +608,6 @@ export const Tasks = (
609608
}
610609
};
611610

612-
// Handle adding a tag
613-
const handleAddTag = () => {
614-
if (tagInput && !newTask.tags.includes(tagInput, 0)) {
615-
setNewTask({ ...newTask, tags: [...newTask.tags, tagInput] });
616-
setTagInput(''); // Clear the input field
617-
}
618-
};
619-
620-
// Handle adding a tag while editing
621-
const handleAddEditTag = () => {
622-
if (editTagInput && !editedTags.includes(editTagInput, 0)) {
623-
setEditedTags([...editedTags, editTagInput]);
624-
setEditTagInput('');
625-
}
626-
};
627-
628611
// Handle removing a tag
629612
const handleRemoveTag = (tagToRemove: string) => {
630613
setNewTask({
@@ -633,11 +616,6 @@ export const Tasks = (
633616
});
634617
};
635618

636-
// Handle removing a tag while editing task
637-
const handleRemoveEditTag = (tagToRemove: string) => {
638-
setEditedTags(editedTags.filter((tag) => tag !== tagToRemove));
639-
};
640-
641619
const sortWithOverdueOnTop = (tasks: Task[]) => {
642620
return [...tasks].sort((a, b) => {
643621
const aOverdue = a.status === 'pending' && isOverdue(a.due);
@@ -723,8 +701,7 @@ export const Tasks = (
723701
task.depends || []
724702
);
725703

726-
setIsEditingTags(false);
727-
setEditTagInput('');
704+
setIsEditingTags(false); // Exit editing mode
728705
};
729706

730707
const handleCancelTags = () => {
@@ -1062,18 +1039,16 @@ export const Tasks = (
10621039
>
10631040
Tags
10641041
</Label>
1065-
<Input
1066-
id="tags"
1067-
name="tags"
1068-
placeholder="Add a tag"
1069-
value={tagInput}
1070-
onChange={(e) => setTagInput(e.target.value)}
1071-
onKeyDown={(e) =>
1072-
e.key === 'Enter' && handleAddTag()
1073-
} // Allow adding tag on pressing Enter
1074-
required
1075-
className="col-span-3"
1076-
/>
1042+
<div className="col-span-3">
1043+
<TagSelector
1044+
options={uniqueTags}
1045+
selected={newTask.tags}
1046+
onChange={(updated) =>
1047+
setNewTask({ ...newTask, tags: updated })
1048+
}
1049+
placeholder="Select or Create Tags"
1050+
/>
1051+
</div>
10771052
</div>
10781053

10791054
<div className="mt-2">
@@ -1924,86 +1899,31 @@ export const Tasks = (
19241899
<TableCell>Tags:</TableCell>
19251900
<TableCell>
19261901
{isEditingTags ? (
1927-
<div>
1928-
<div className="flex items-center w-full">
1929-
<Input
1930-
type="text"
1931-
value={editTagInput}
1932-
onChange={(e) => {
1933-
// For allowing only alphanumeric characters
1934-
if (
1935-
e.target.value.length > 1
1936-
) {
1937-
/^[a-zA-Z0-9]*$/.test(
1938-
e.target.value.trim()
1939-
)
1940-
? setEditTagInput(
1941-
e.target.value.trim()
1942-
)
1943-
: '';
1944-
} else {
1945-
/^[a-zA-Z]*$/.test(
1946-
e.target.value.trim()
1947-
)
1948-
? setEditTagInput(
1949-
e.target.value.trim()
1950-
)
1951-
: '';
1952-
}
1953-
}}
1954-
placeholder="Add a tag (press enter to add)"
1955-
className="flex-grow mr-2"
1956-
onKeyDown={(e) =>
1957-
e.key === 'Enter' &&
1958-
handleAddEditTag()
1959-
}
1960-
/>
1961-
<Button
1962-
variant="ghost"
1963-
size="icon"
1964-
onClick={() =>
1965-
handleSaveTags(task)
1966-
}
1967-
aria-label="Save tags"
1968-
>
1969-
<CheckIcon className="h-4 w-4 text-green-500" />
1970-
</Button>
1971-
<Button
1972-
variant="ghost"
1973-
size="icon"
1974-
onClick={handleCancelTags}
1975-
aria-label="Cancel editing tags"
1976-
>
1977-
<XIcon className="h-4 w-4 text-red-500" />
1978-
</Button>
1979-
</div>
1980-
<div className="mt-2">
1981-
{editedTags != null &&
1982-
editedTags.length > 0 && (
1983-
<div>
1984-
<div className="flex flex-wrap gap-2 col-span-3">
1985-
{editedTags.map(
1986-
(tag, index) => (
1987-
<Badge key={index}>
1988-
<span>{tag}</span>
1989-
<button
1990-
type="button"
1991-
className="ml-2 text-red-500"
1992-
onClick={() =>
1993-
handleRemoveEditTag(
1994-
tag
1995-
)
1996-
}
1997-
>
1998-
1999-
</button>
2000-
</Badge>
2001-
)
2002-
)}
2003-
</div>
2004-
</div>
2005-
)}
2006-
</div>
1902+
<div className="flex items-center">
1903+
<TagSelector
1904+
options={uniqueTags}
1905+
selected={editedTags}
1906+
onChange={(updated) =>
1907+
setEditedTags(updated)
1908+
}
1909+
placeholder="Select or Create Tags"
1910+
/>
1911+
<Button
1912+
variant="ghost"
1913+
size="icon"
1914+
onClick={() =>
1915+
handleSaveTags(task)
1916+
}
1917+
>
1918+
<CheckIcon className="h-4 w-4 text-green-500" />
1919+
</Button>
1920+
<Button
1921+
variant="ghost"
1922+
size="icon"
1923+
onClick={handleCancelTags}
1924+
>
1925+
<XIcon className="h-4 w-4 text-red-500" />
1926+
</Button>
20071927
</div>
20081928
) : (
20091929
<div className="flex items-center flex-wrap">

0 commit comments

Comments
 (0)