Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 85 additions & 19 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const Tasks = (
due: '',
tags: [] as string[],
});
const [isCreatingNewProject, setIsCreatingNewProject] = useState(false);
const [isAddTaskOpen, setIsAddTaskOpen] = useState(false);
const [_isDialogOpen, setIsDialogOpen] = useState(false);
const [tagInput, setTagInput] = useState('');
Expand Down Expand Up @@ -286,6 +287,12 @@ export const Tasks = (
fetchTasksForEmail();
}, [props.email]);

useEffect(() => {
if (!isAddTaskOpen) {
setIsCreatingNewProject(false);
}
}, [isAddTaskOpen]);

syncTasksWithTwAndDb = useCallback(async () => {
try {
const { email: user_email, encryptionSecret, UUID } = props;
Expand All @@ -308,8 +315,16 @@ export const Tasks = (
.where('email')
.equals(user_email)
.toArray();
setTasks(sortTasksById(updatedTasks, 'desc'));
setTempTasks(sortTasksById(updatedTasks, 'desc'));
const sortedTasks = sortTasksById(updatedTasks, 'desc');
setTasks(sortedTasks);
setTempTasks(sortedTasks);

// Update unique projects after a successful sync so the Project dropdown is populated
const projectsSet = new Set(sortedTasks.map((task) => task.project));
const filteredProjects = Array.from(projectsSet)
.filter((project) => project !== '')
.sort((a, b) => (a > b ? 1 : -1));
setUniqueProjects(filteredProjects);
});

// Store last sync timestamp using hashed key
Expand Down Expand Up @@ -1075,25 +1090,76 @@ export const Tasks = (
</div>

<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="description"
className="text-right"
>
<Label htmlFor="project" className="text-right">
Project
</Label>
<Input
id="project"
name="project"
type=""
value={newTask.project}
onChange={(e) =>
setNewTask({
...newTask,
project: e.target.value,
})
}
className="col-span-3"
/>
<div className="col-span-3 space-y-2">
<Select
value={
isCreatingNewProject
? ''
: newTask.project || ''
}
onValueChange={(value: string) => {
if (value === '') {
// User selected "create new project" option
setIsCreatingNewProject(true);
setNewTask({ ...newTask, project: '' });
} else {
// User selected an existing project
setIsCreatingNewProject(false);
setNewTask({
...newTask,
project: value,
});
}
}}
>
<SelectTrigger id="project">
<SelectValue
placeholder={
uniqueProjects.length
? 'Select a project'
: 'No projects yet'
}
/>
</SelectTrigger>
<SelectContent>
{uniqueProjects.map((project: string) => (
<SelectItem
key={project}
value={project}
data-testid={`project-option-${project}`}
>
{project}
</SelectItem>
))}
<SelectItem
value=""
data-testid="project-option-create"
>
+ Create new project…
</SelectItem>
</SelectContent>
</Select>
{isCreatingNewProject && (
<Input
id="project-name"
name="project"
placeholder="New project name"
value={newTask.project}
autoFocus
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) =>
setNewTask({
...newTask,
project: e.target.value,
})
}
/>
)}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="due" className="text-right">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@ jest.mock('@/components/ui/multi-select', () => ({
)),
}));

jest.mock('@/components/ui/select', () => {
return {
Select: ({ children, onValueChange, value }: any) => (
<select
data-testid="project-select"
value={value}
onChange={(e) => onValueChange?.(e.target.value)}
>
{children}
</select>
),
SelectTrigger: ({ children }: any) => <>{children}</>,
SelectValue: ({ placeholder }: any) => (
<option value="" disabled hidden>
{placeholder}
</option>
),
SelectContent: ({ children }: any) => <>{children}</>,
SelectItem: ({ value, children, ...props }: any) => (
<option value={value} {...props}>
{children}
</option>
),
};
});

jest.mock('../../BottomBar/BottomBar', () => {
return jest.fn(() => <div>Mocked BottomBar</div>);
});
Expand Down Expand Up @@ -421,4 +447,42 @@ describe('Tasks Component', () => {
const firstRow = screen.getAllByRole('row')[1];
expect(within(firstRow).getByText('Task 1')).toBeInTheDocument();
});

test('project dropdown lists existing projects and create-new option', async () => {
render(<Tasks {...mockProps} />);

expect(await screen.findByText('Task 1')).toBeInTheDocument();

const addTaskButton = screen.getByRole('button', { name: /add task/i });
fireEvent.click(addTaskButton);

const projectSelect = await screen.findByTestId('project-select');
expect(
within(projectSelect).getByText('Select a project')
).toBeInTheDocument();
expect(within(projectSelect).getByText('Engineering')).toBeInTheDocument();
expect(within(projectSelect).getByText('ProjectA')).toBeInTheDocument();
expect(
within(projectSelect).getByText('+ Create new project…')
).toBeInTheDocument();
});

test('selecting "+ Create new project…" reveals inline input', async () => {
render(<Tasks {...mockProps} />);

expect(await screen.findByText('Task 1')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: /add task/i }));

const projectSelect = await screen.findByTestId('project-select');
fireEvent.change(projectSelect, { target: { value: '' } }); // Empty string triggers "create new project" mode

const newProjectInput =
await screen.findByPlaceholderText('New project name');
fireEvent.change(newProjectInput, {
target: { value: 'My Fresh Project' },
});

expect(newProjectInput).toHaveValue('My Fresh Project');
});
});
Loading