Skip to content

Commit cd36ddc

Browse files
feat: enhance tag editing functionality in task component (#208)
* feat: enhance tag editing functionality in task component * add tests for tag editing functionality in task dialog * fix prettier issues --------- Co-authored-by: Parth Kshirsagar <parthkshirsagar96@gmail.com>
1 parent 064ba32 commit cd36ddc

2 files changed

Lines changed: 223 additions & 45 deletions

File tree

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

Lines changed: 111 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const Tasks = (
111111
const [editedTags, setEditedTags] = useState<string[]>(
112112
_selectedTask?.tags || []
113113
);
114+
const [editTagInput, setEditTagInput] = useState<string>('');
114115
const [isEditingTags, setIsEditingTags] = useState(false);
115116
const [isEditingPriority, setIsEditingPriority] = useState(false);
116117
const [editedPriority, setEditedPriority] = useState('NONE');
@@ -550,6 +551,14 @@ export const Tasks = (
550551
}
551552
};
552553

554+
// Handle adding a tag while editing
555+
const handleAddEditTag = () => {
556+
if (editTagInput && !editedTags.includes(editTagInput, 0)) {
557+
setEditedTags([...editedTags, editTagInput]);
558+
setEditTagInput('');
559+
}
560+
};
561+
553562
// Handle removing a tag
554563
const handleRemoveTag = (tagToRemove: string) => {
555564
setNewTask({
@@ -558,6 +567,11 @@ export const Tasks = (
558567
});
559568
};
560569

570+
// Handle removing a tag while editing task
571+
const handleRemoveEditTag = (tagToRemove: string) => {
572+
setEditedTags(editedTags.filter((tag) => tag !== tagToRemove));
573+
};
574+
561575
const sortWithOverdueOnTop = (tasks: Task[]) => {
562576
return [...tasks].sort((a, b) => {
563577
const aOverdue = a.status === 'pending' && isOverdue(a.due);
@@ -631,6 +645,7 @@ export const Tasks = (
631645
);
632646

633647
setIsEditingTags(false); // Exit editing mode
648+
setEditTagInput(''); // Reset edit tag input
634649
};
635650

636651
const handleCancelTags = () => {
@@ -911,19 +926,22 @@ export const Tasks = (
911926

912927
<div className="mt-2">
913928
{newTask.tags.length > 0 && (
914-
<div className="flex flex-wrap gap-2">
915-
{newTask.tags.map((tag, index) => (
916-
<Badge key={index}>
917-
<span>{tag}</span>
918-
<button
919-
type="button"
920-
className="ml-2 text-red-500"
921-
onClick={() => handleRemoveTag(tag)}
922-
>
923-
924-
</button>
925-
</Badge>
926-
))}
929+
<div className="grid grid-cols-4 items-center">
930+
<div> </div>
931+
<div className="flex flex-wrap gap-2 col-span-3">
932+
{newTask.tags.map((tag, index) => (
933+
<Badge key={index}>
934+
<span>{tag}</span>
935+
<button
936+
type="button"
937+
className="ml-2 text-red-500"
938+
onClick={() => handleRemoveTag(tag)}
939+
>
940+
941+
</button>
942+
</Badge>
943+
))}
944+
</div>
927945
</div>
928946
)}
929947
</div>
@@ -1561,45 +1579,94 @@ export const Tasks = (
15611579
<TableCell>Tags:</TableCell>
15621580
<TableCell>
15631581
{isEditingTags ? (
1564-
<div className="flex items-center">
1565-
<Input
1566-
type="text"
1567-
value={editedTags.join(', ')}
1568-
onChange={(e) =>
1569-
setEditedTags(
1570-
e.target.value
1571-
.split(',')
1572-
.map((tag) => tag.trim())
1573-
)
1574-
}
1575-
className="flex-grow mr-2"
1576-
/>
1577-
<Button
1578-
variant="ghost"
1579-
size="icon"
1580-
onClick={() =>
1581-
handleSaveTags(task)
1582-
}
1583-
>
1584-
<CheckIcon className="h-4 w-4 text-green-500" />
1585-
</Button>
1586-
<Button
1587-
variant="ghost"
1588-
size="icon"
1589-
onClick={handleCancelTags}
1590-
>
1591-
<XIcon className="h-4 w-4 text-red-500" />
1592-
</Button>
1582+
<div>
1583+
<div className="flex items-center w-full">
1584+
<Input
1585+
type="text"
1586+
value={editTagInput}
1587+
onChange={(e) => {
1588+
// For allowing only alphanumeric characters
1589+
if (
1590+
e.target.value.length > 1
1591+
) {
1592+
/^[a-zA-Z0-9]*$/.test(
1593+
e.target.value.trim()
1594+
)
1595+
? setEditTagInput(
1596+
e.target.value.trim()
1597+
)
1598+
: '';
1599+
} else {
1600+
/^[a-zA-Z]*$/.test(
1601+
e.target.value.trim()
1602+
)
1603+
? setEditTagInput(
1604+
e.target.value.trim()
1605+
)
1606+
: '';
1607+
}
1608+
}}
1609+
placeholder="Add a tag (press enter to add)"
1610+
className="flex-grow mr-2"
1611+
onKeyDown={(e) =>
1612+
e.key === 'Enter' &&
1613+
handleAddEditTag()
1614+
}
1615+
/>
1616+
<Button
1617+
variant="ghost"
1618+
size="icon"
1619+
onClick={() =>
1620+
handleSaveTags(task)
1621+
}
1622+
>
1623+
<CheckIcon className="h-4 w-4 text-green-500" />
1624+
</Button>
1625+
<Button
1626+
variant="ghost"
1627+
size="icon"
1628+
onClick={handleCancelTags}
1629+
>
1630+
<XIcon className="h-4 w-4 text-red-500" />
1631+
</Button>
1632+
</div>
1633+
<div className="mt-2">
1634+
{editedTags != null &&
1635+
editedTags.length > 0 && (
1636+
<div>
1637+
<div className="flex flex-wrap gap-2 col-span-3">
1638+
{editedTags.map(
1639+
(tag, index) => (
1640+
<Badge key={index}>
1641+
<span>{tag}</span>
1642+
<button
1643+
type="button"
1644+
className="ml-2 text-red-500"
1645+
onClick={() =>
1646+
handleRemoveEditTag(
1647+
tag
1648+
)
1649+
}
1650+
>
1651+
1652+
</button>
1653+
</Badge>
1654+
)
1655+
)}
1656+
</div>
1657+
</div>
1658+
)}
1659+
</div>
15931660
</div>
15941661
) : (
1595-
<div className="flex items-center">
1662+
<div className="flex items-center flex-wrap">
15961663
{task.tags !== null &&
15971664
task.tags.length >= 1 ? (
15981665
task.tags.map((tag, index) => (
15991666
<Badge
16001667
key={index}
16011668
variant="secondary"
1602-
className="mr-2"
1669+
className="mr-2 mt-1"
16031670
>
16041671
<Tag className="pr-3" />
16051672
{tag}

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

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from '@testing-library/react';
1+
import { render, screen, fireEvent, within } from '@testing-library/react';
22
import { Tasks } from '../Tasks';
33

44
// Mock props for the Tasks component
@@ -146,6 +146,117 @@ describe('Tasks Component', () => {
146146
expect(screen.getByTestId('current-page')).toHaveTextContent('1');
147147
});
148148

149+
test('shows tags as badges in task dialog and allows editing (add on Enter)', async () => {
150+
render(<Tasks {...mockProps} />);
151+
152+
expect(await screen.findByText('Task 1')).toBeInTheDocument();
153+
154+
const taskRow = screen.getByText('Task 1');
155+
fireEvent.click(taskRow);
156+
157+
expect(await screen.findByText('Tags:')).toBeInTheDocument();
158+
159+
expect(screen.getByText('tag1')).toBeInTheDocument();
160+
161+
const tagsLabel = screen.getByText('Tags:');
162+
const tagsRow = tagsLabel.closest('tr') as HTMLElement;
163+
const pencilButton = within(tagsRow).getByRole('button');
164+
fireEvent.click(pencilButton);
165+
166+
const editInput = await screen.findByPlaceholderText(
167+
'Add a tag (press enter to add)'
168+
);
169+
170+
fireEvent.change(editInput, { target: { value: 'newtag' } });
171+
fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' });
172+
173+
expect(await screen.findByText('newtag')).toBeInTheDocument();
174+
175+
expect((editInput as HTMLInputElement).value).toBe('');
176+
});
177+
178+
test('adds a tag while editing and saves updated tags to backend', async () => {
179+
render(<Tasks {...mockProps} />);
180+
181+
expect(await screen.findByText('Task 1')).toBeInTheDocument();
182+
183+
const taskRow = screen.getByText('Task 1');
184+
fireEvent.click(taskRow);
185+
186+
expect(await screen.findByText('Tags:')).toBeInTheDocument();
187+
188+
const tagsLabel = screen.getByText('Tags:');
189+
const tagsRow = tagsLabel.closest('tr') as HTMLElement;
190+
const pencilButton = within(tagsRow).getByRole('button');
191+
fireEvent.click(pencilButton);
192+
193+
const editInput = await screen.findByPlaceholderText(
194+
'Add a tag (press enter to add)'
195+
);
196+
197+
fireEvent.change(editInput, { target: { value: 'addedtag' } });
198+
fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' });
199+
200+
expect(await screen.findByText('addedtag')).toBeInTheDocument();
201+
202+
const actionContainer = editInput.parentElement as HTMLElement;
203+
const actionButtons = within(actionContainer).getAllByRole('button');
204+
fireEvent.click(actionButtons[0]);
205+
206+
const hooks = require('../hooks');
207+
expect(hooks.editTaskOnBackend).toHaveBeenCalled();
208+
209+
const callArg = hooks.editTaskOnBackend.mock.calls[0][0];
210+
expect(callArg.tags).toEqual(expect.arrayContaining(['tag1', 'addedtag']));
211+
});
212+
213+
test('removes a tag while editing and saves updated tags to backend', async () => {
214+
render(<Tasks {...mockProps} />);
215+
216+
expect(await screen.findByText('Task 1')).toBeInTheDocument();
217+
218+
const taskRow = screen.getByText('Task 1');
219+
fireEvent.click(taskRow);
220+
221+
expect(await screen.findByText('Tags:')).toBeInTheDocument();
222+
223+
const tagsLabel = screen.getByText('Tags:');
224+
const tagsRow = tagsLabel.closest('tr') as HTMLElement;
225+
const pencilButton = within(tagsRow).getByRole('button');
226+
fireEvent.click(pencilButton);
227+
228+
const editInput = await screen.findByPlaceholderText(
229+
'Add a tag (press enter to add)'
230+
);
231+
232+
fireEvent.change(editInput, { target: { value: 'newtag' } });
233+
fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' });
234+
235+
expect(await screen.findByText('newtag')).toBeInTheDocument();
236+
237+
const tagBadge = screen.getByText('tag1');
238+
const badgeContainer = (tagBadge.closest('div') ||
239+
tagBadge.parentElement) as HTMLElement;
240+
241+
const removeButton = within(badgeContainer).getByText('✖');
242+
fireEvent.click(removeButton);
243+
244+
expect(screen.queryByText('tag2')).not.toBeInTheDocument();
245+
246+
const actionContainer = editInput.parentElement as HTMLElement;
247+
248+
const actionButtons = within(actionContainer).getAllByRole('button');
249+
250+
fireEvent.click(actionButtons[0]);
251+
252+
const hooks = require('../hooks');
253+
expect(hooks.editTaskOnBackend).toHaveBeenCalled();
254+
255+
const callArg = hooks.editTaskOnBackend.mock.calls[0][0];
256+
257+
expect(callArg.tags).toEqual(expect.arrayContaining(['newtag', '-tag1']));
258+
});
259+
149260
test('shows red background on task ID and Overdue badge for overdue tasks', async () => {
150261
render(<Tasks {...mockProps} />);
151262

0 commit comments

Comments
 (0)