diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 2ab48ed5..50bce8c8 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -111,6 +111,7 @@ export const Tasks = ( const [editedTags, setEditedTags] = useState( _selectedTask?.tags || [] ); + const [editTagInput, setEditTagInput] = useState(''); const [isEditingTags, setIsEditingTags] = useState(false); const [isEditingPriority, setIsEditingPriority] = useState(false); const [editedPriority, setEditedPriority] = useState('NONE'); @@ -550,6 +551,14 @@ export const Tasks = ( } }; + // Handle adding a tag while editing + const handleAddEditTag = () => { + if (editTagInput && !editedTags.includes(editTagInput, 0)) { + setEditedTags([...editedTags, editTagInput]); + setEditTagInput(''); + } + }; + // Handle removing a tag const handleRemoveTag = (tagToRemove: string) => { setNewTask({ @@ -558,6 +567,11 @@ export const Tasks = ( }); }; + // Handle removing a tag while editing task + const handleRemoveEditTag = (tagToRemove: string) => { + setEditedTags(editedTags.filter((tag) => tag !== tagToRemove)); + }; + const sortWithOverdueOnTop = (tasks: Task[]) => { return [...tasks].sort((a, b) => { const aOverdue = a.status === 'pending' && isOverdue(a.due); @@ -631,6 +645,7 @@ export const Tasks = ( ); setIsEditingTags(false); // Exit editing mode + setEditTagInput(''); // Reset edit tag input }; const handleCancelTags = () => { @@ -911,19 +926,22 @@ export const Tasks = (
{newTask.tags.length > 0 && ( -
- {newTask.tags.map((tag, index) => ( - - {tag} - - - ))} +
+
+
+ {newTask.tags.map((tag, index) => ( + + {tag} + + + ))} +
)}
@@ -1561,45 +1579,94 @@ export const Tasks = ( Tags: {isEditingTags ? ( -
- - setEditedTags( - e.target.value - .split(',') - .map((tag) => tag.trim()) - ) - } - className="flex-grow mr-2" - /> - - +
+
+ { + // For allowing only alphanumeric characters + if ( + e.target.value.length > 1 + ) { + /^[a-zA-Z0-9]*$/.test( + e.target.value.trim() + ) + ? setEditTagInput( + e.target.value.trim() + ) + : ''; + } else { + /^[a-zA-Z]*$/.test( + e.target.value.trim() + ) + ? setEditTagInput( + e.target.value.trim() + ) + : ''; + } + }} + placeholder="Add a tag (press enter to add)" + className="flex-grow mr-2" + onKeyDown={(e) => + e.key === 'Enter' && + handleAddEditTag() + } + /> + + +
+
+ {editedTags != null && + editedTags.length > 0 && ( +
+
+ {editedTags.map( + (tag, index) => ( + + {tag} + + + ) + )} +
+
+ )} +
) : ( -
+
{task.tags !== null && task.tags.length >= 1 ? ( task.tags.map((tag, index) => ( {tag} diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index 546c14f1..2ba866d9 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, within } from '@testing-library/react'; import { Tasks } from '../Tasks'; // Mock props for the Tasks component @@ -146,6 +146,117 @@ describe('Tasks Component', () => { expect(screen.getByTestId('current-page')).toHaveTextContent('1'); }); + test('shows tags as badges in task dialog and allows editing (add on Enter)', async () => { + render(); + + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + + const taskRow = screen.getByText('Task 1'); + fireEvent.click(taskRow); + + expect(await screen.findByText('Tags:')).toBeInTheDocument(); + + expect(screen.getByText('tag1')).toBeInTheDocument(); + + const tagsLabel = screen.getByText('Tags:'); + const tagsRow = tagsLabel.closest('tr') as HTMLElement; + const pencilButton = within(tagsRow).getByRole('button'); + fireEvent.click(pencilButton); + + const editInput = await screen.findByPlaceholderText( + 'Add a tag (press enter to add)' + ); + + fireEvent.change(editInput, { target: { value: 'newtag' } }); + fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' }); + + expect(await screen.findByText('newtag')).toBeInTheDocument(); + + expect((editInput as HTMLInputElement).value).toBe(''); + }); + + test('adds a tag while editing and saves updated tags to backend', async () => { + render(); + + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + + const taskRow = screen.getByText('Task 1'); + fireEvent.click(taskRow); + + expect(await screen.findByText('Tags:')).toBeInTheDocument(); + + const tagsLabel = screen.getByText('Tags:'); + const tagsRow = tagsLabel.closest('tr') as HTMLElement; + const pencilButton = within(tagsRow).getByRole('button'); + fireEvent.click(pencilButton); + + const editInput = await screen.findByPlaceholderText( + 'Add a tag (press enter to add)' + ); + + fireEvent.change(editInput, { target: { value: 'addedtag' } }); + fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' }); + + expect(await screen.findByText('addedtag')).toBeInTheDocument(); + + const actionContainer = editInput.parentElement as HTMLElement; + const actionButtons = within(actionContainer).getAllByRole('button'); + fireEvent.click(actionButtons[0]); + + const hooks = require('../hooks'); + expect(hooks.editTaskOnBackend).toHaveBeenCalled(); + + const callArg = hooks.editTaskOnBackend.mock.calls[0][0]; + expect(callArg.tags).toEqual(expect.arrayContaining(['tag1', 'addedtag'])); + }); + + test('removes a tag while editing and saves updated tags to backend', async () => { + render(); + + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + + const taskRow = screen.getByText('Task 1'); + fireEvent.click(taskRow); + + expect(await screen.findByText('Tags:')).toBeInTheDocument(); + + const tagsLabel = screen.getByText('Tags:'); + const tagsRow = tagsLabel.closest('tr') as HTMLElement; + const pencilButton = within(tagsRow).getByRole('button'); + fireEvent.click(pencilButton); + + const editInput = await screen.findByPlaceholderText( + 'Add a tag (press enter to add)' + ); + + fireEvent.change(editInput, { target: { value: 'newtag' } }); + fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' }); + + expect(await screen.findByText('newtag')).toBeInTheDocument(); + + const tagBadge = screen.getByText('tag1'); + const badgeContainer = (tagBadge.closest('div') || + tagBadge.parentElement) as HTMLElement; + + const removeButton = within(badgeContainer).getByText('✖'); + fireEvent.click(removeButton); + + expect(screen.queryByText('tag2')).not.toBeInTheDocument(); + + const actionContainer = editInput.parentElement as HTMLElement; + + const actionButtons = within(actionContainer).getAllByRole('button'); + + fireEvent.click(actionButtons[0]); + + const hooks = require('../hooks'); + expect(hooks.editTaskOnBackend).toHaveBeenCalled(); + + const callArg = hooks.editTaskOnBackend.mock.calls[0][0]; + + expect(callArg.tags).toEqual(expect.arrayContaining(['newtag', '-tag1'])); + }); + test('shows red background on task ID and Overdue badge for overdue tasks', async () => { render();