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
154 changes: 143 additions & 11 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import { Task } from '../../utils/types';
import { ReportsView } from './ReportsView';
import Fuse from 'fuse.js';
import { useHotkeys } from '@/hooks/useHotkeys';
import {
Table,
TableBody,
Expand Down Expand Up @@ -72,6 +73,7 @@ import { debounce } from '@/components/utils/utils';
import { DatePicker } from '@/components/ui/date-picker';
import { format } from 'date-fns';
import { Taskskeleton } from './Task-Skeleton';
import { Key } from '@/components/ui/key-button';

const db = new TasksDatabase();
export let syncTasksWithTwAndDb: () => any;
Expand Down Expand Up @@ -135,6 +137,9 @@ export const Tasks = (
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState('');
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null);
const tableRef = useRef<HTMLDivElement>(null);
const [hotkeysEnabled, setHotkeysEnabled] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);

const isOverdue = (due?: string) => {
if (!due) return false;
Expand Down Expand Up @@ -182,6 +187,42 @@ export const Tasks = (
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const totalPages = Math.ceil(tempTasks.length / tasksPerPage) || 1;

useEffect(() => {
const handler = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement ||
_isDialogOpen ||
target.isContentEditable
) {
return;
}

if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => Math.min(prev + 1, currentTasks.length - 1));
}

if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => Math.max(prev - 1, 0));
}

if (e.key === 'e') {
e.preventDefault();
const task = currentTasks[selectedIndex];
if (task) {
document.getElementById(`task-row-${task.id}`)?.click();
}
}
};

window.addEventListener('keydown', handler, true);
return () => window.removeEventListener('keydown', handler, true);
}, [hotkeysEnabled, selectedIndex, currentTasks]);

useEffect(() => {
const hashedKey = hashKey('tasksPerPage', props.email);
const storedTasksPerPage = localStorage.getItem(hashedKey);
Expand Down Expand Up @@ -729,6 +770,73 @@ export const Tasks = (
}
};

useHotkeys(['f'], () => {
if (!showReports) {
document.getElementById('search')?.focus();
}
});
useHotkeys(['a'], () => {
if (!showReports) {
document.getElementById('add-new-task')?.click();
}
});
useHotkeys(['r'], () => {
if (!showReports) {
document.getElementById('sync-task')?.click();
}
});
useHotkeys(['c'], () => {
if (!showReports && !_isDialogOpen) {
const task = currentTasks[selectedIndex];
if (!task) return;
// Step 1
const openBtn = document.getElementById(`task-row-${task.id}`);
openBtn?.click();
// Step 2
setTimeout(() => {
const confirmBtn = document.getElementById(
`mark-task-complete-${task.id}`
);
confirmBtn?.click();
}, 200);
} else {
if (_isDialogOpen) {
const task = currentTasks[selectedIndex];
if (!task) return;
const confirmBtn = document.getElementById(
`mark-task-complete-${task.id}`
);
confirmBtn?.click();
}
}
});

useHotkeys(['d'], () => {
if (!showReports && !_isDialogOpen) {
const task = currentTasks[selectedIndex];
if (!task) return;
// Step 1
const openBtn = document.getElementById(`task-row-${task.id}`);
openBtn?.click();
// Step 2
setTimeout(() => {
const confirmBtn = document.getElementById(
`mark-task-as-deleted-${task.id}`
);
confirmBtn?.click();
}, 200);
} else {
if (_isDialogOpen) {
const task = currentTasks[selectedIndex];
if (!task) return;
const confirmBtn = document.getElementById(
`mark-task-as-deleted-${task.id}`
);
confirmBtn?.click();
}
}
});

return (
<section
id="tasks"
Expand Down Expand Up @@ -779,7 +887,11 @@ export const Tasks = (
{showReports ? (
<ReportsView tasks={tasks} />
) : (
<>
<div
ref={tableRef}
onMouseEnter={() => setHotkeysEnabled(true)}
onMouseLeave={() => setHotkeysEnabled(false)}
>
{tasks.length != 0 ? (
<>
<div className="mt-10 pl-1 md:pl-4 pr-1 md:pr-4 bg-muted/50 border shadow-md rounded-lg p-4 h-full pt-12 pb-6">
Expand All @@ -793,12 +905,14 @@ export const Tasks = (
</h3>
<div className="hidden sm:flex flex-row w-full items-center gap-2 md:gap-4">
<Input
id="search"
type="text"
placeholder="Search tasks..."
value={searchTerm}
onChange={handleSearchChange}
className="flex-1 min-w-[150px]"
data-testid="task-search-bar"
icon={<Key lable="f" />}
/>
<MultiSelectFilter
title="Projects"
Expand Down Expand Up @@ -828,10 +942,12 @@ export const Tasks = (
>
<DialogTrigger asChild>
<Button
id="add-new-task"
variant="outline"
onClick={() => setIsAddTaskOpen(true)}
>
Add Task
<Key lable="a" />
</Button>
</DialogTrigger>
<DialogContent>
Expand Down Expand Up @@ -1013,13 +1129,15 @@ export const Tasks = (
</div>
<div className="flex flex-col items-end gap-2">
<Button
id="sync-task"
variant="outline"
onClick={() => (
props.setIsLoading(true),
syncTasksWithTwAndDb()
)}
>
Sync
<Key lable="r" />
</Button>
</div>
</div>
Expand Down Expand Up @@ -1079,7 +1197,11 @@ export const Tasks = (
key={index}
>
<DialogTrigger asChild>
<TableRow key={index} className="border-b">
<TableRow
id={`task-row-${task.id}`}
key={index}
className={`border-b cursor-pointer ${selectedIndex === index ? 'bg-muted/50' : ''}`}
>
{/* Display task details */}
<TableCell className="py-2">
<span
Expand Down Expand Up @@ -1842,13 +1964,15 @@ export const Tasks = (
onClick={() =>
handleSaveTags(task)
}
aria-label="Save tags"
>
<CheckIcon className="h-4 w-4 text-green-500" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleCancelTags}
aria-label="Cancel editing tags"
>
<XIcon className="h-4 w-4 text-red-500" />
</Button>
Expand Down Expand Up @@ -2033,7 +2157,11 @@ export const Tasks = (
{task.status == 'pending' ? (
<Dialog>
<DialogTrigger asChild className="mr-5">
<Button>Mark As Completed</Button>
<Button
id={`mark-task-complete-${task.id}`}
>
Mark As Completed <Key lable="c" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
Expand All @@ -2048,14 +2176,15 @@ export const Tasks = (
<DialogClose asChild>
<Button
className="mr-5"
onClick={() =>
onClick={() => {
markTaskAsCompleted(
props.email,
props.encryptionSecret,
props.UUID,
task.uuid
)
}
);
setIsDialogOpen(false);
}}
>
Yes
</Button>
Expand All @@ -2074,10 +2203,12 @@ export const Tasks = (
<Dialog>
<DialogTrigger asChild>
<Button
id={`mark-task-as-deleted-${task.id}`}
className="mr-4"
variant={'destructive'}
>
<Trash2Icon />
<Key lable="d" />
</Button>
</DialogTrigger>
<DialogContent>
Expand All @@ -2093,14 +2224,15 @@ export const Tasks = (
<DialogClose asChild>
<Button
className="mr-5"
onClick={() =>
onClick={() => {
markTaskAsDeleted(
props.email,
props.encryptionSecret,
props.UUID,
task.uuid
)
}
);
setIsDialogOpen(false);
}}
>
Yes
</Button>
Expand Down Expand Up @@ -2358,7 +2490,7 @@ export const Tasks = (
</div>
</>
)}
</>
</div>
)}
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { render, screen, fireEvent, act, within } from '@testing-library/react';
import {
render,
screen,
fireEvent,
act,
within,
waitFor,
} from '@testing-library/react';
import { Tasks } from '../Tasks';

// Mock props for the Tasks component
Expand Down Expand Up @@ -225,13 +232,17 @@ describe('Tasks Component', () => {

expect(await screen.findByText('addedtag')).toBeInTheDocument();

const actionContainer = editInput.parentElement as HTMLElement;
const actionButtons = within(actionContainer).getAllByRole('button');
fireEvent.click(actionButtons[0]);
const saveButton = await screen.findByRole('button', {
name: /save tags/i,
});
fireEvent.click(saveButton);

const hooks = require('../hooks');
expect(hooks.editTaskOnBackend).toHaveBeenCalled();
await waitFor(() => {
const hooks = require('../hooks');
expect(hooks.editTaskOnBackend).toHaveBeenCalled();
});

const hooks = require('../hooks');
const callArg = hooks.editTaskOnBackend.mock.calls[0][0];
expect(callArg.tags).toEqual(expect.arrayContaining(['tag1', 'addedtag']));
});
Expand Down Expand Up @@ -267,17 +278,19 @@ describe('Tasks Component', () => {
const removeButton = within(badgeContainer).getByText('✖');
fireEvent.click(removeButton);

expect(screen.queryByText('tag2')).not.toBeInTheDocument();
expect(screen.queryByText('tag1')).not.toBeInTheDocument();

const actionContainer = editInput.parentElement as HTMLElement;

const actionButtons = within(actionContainer).getAllByRole('button');
const saveButton = await screen.findByRole('button', {
name: /save tags/i,
});
fireEvent.click(saveButton);

fireEvent.click(actionButtons[0]);
await waitFor(() => {
const hooks = require('../hooks');
expect(hooks.editTaskOnBackend).toHaveBeenCalled();
});

const hooks = require('../hooks');
expect(hooks.editTaskOnBackend).toHaveBeenCalled();

const callArg = hooks.editTaskOnBackend.mock.calls[0][0];

expect(callArg.tags).toEqual(expect.arrayContaining(['newtag', '-tag1']));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const buttonVariants = cva(
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
default: 'h-10 px-3 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
Expand Down
Loading
Loading