1- import { useEffect , useState , useCallback } from 'react' ;
1+ import { useEffect , useState , useCallback , useRef } from 'react' ;
22import { Task } from '../../utils/types' ;
33import { ReportsView } from './ReportsView' ;
44import Fuse from 'fuse.js' ;
5+ import { useHotkeys } from '@/hooks/useHotkeys' ;
56import {
67 Table ,
78 TableBody ,
@@ -72,6 +73,7 @@ import { debounce } from '@/components/utils/utils';
7273import { DatePicker } from '@/components/ui/date-picker' ;
7374import { format } from 'date-fns' ;
7475import { Taskskeleton } from './Task-Skeleton' ;
76+ import { Key } from '@/components/ui/key-button' ;
7577
7678const db = new TasksDatabase ( ) ;
7779export let syncTasksWithTwAndDb : ( ) => any ;
@@ -135,6 +137,9 @@ export const Tasks = (
135137 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
136138 const [ debouncedTerm , setDebouncedTerm ] = useState ( '' ) ;
137139 const [ lastSyncTime , setLastSyncTime ] = useState < number | null > ( null ) ;
140+ const tableRef = useRef < HTMLDivElement > ( null ) ;
141+ const [ hotkeysEnabled , setHotkeysEnabled ] = useState ( false ) ;
142+ const [ selectedIndex , setSelectedIndex ] = useState ( 0 ) ;
138143
139144 const isOverdue = ( due ?: string ) => {
140145 if ( ! due ) return false ;
@@ -182,6 +187,42 @@ export const Tasks = (
182187 const paginate = ( pageNumber : number ) => setCurrentPage ( pageNumber ) ;
183188 const totalPages = Math . ceil ( tempTasks . length / tasksPerPage ) || 1 ;
184189
190+ useEffect ( ( ) => {
191+ const handler = ( e : KeyboardEvent ) => {
192+ const target = e . target as HTMLElement ;
193+ if (
194+ target instanceof HTMLInputElement ||
195+ target instanceof HTMLTextAreaElement ||
196+ target instanceof HTMLSelectElement ||
197+ _isDialogOpen ||
198+ target . isContentEditable
199+ ) {
200+ return ;
201+ }
202+
203+ if ( e . key === 'ArrowDown' ) {
204+ e . preventDefault ( ) ;
205+ setSelectedIndex ( ( prev ) => Math . min ( prev + 1 , currentTasks . length - 1 ) ) ;
206+ }
207+
208+ if ( e . key === 'ArrowUp' ) {
209+ e . preventDefault ( ) ;
210+ setSelectedIndex ( ( prev ) => Math . max ( prev - 1 , 0 ) ) ;
211+ }
212+
213+ if ( e . key === 'e' ) {
214+ e . preventDefault ( ) ;
215+ const task = currentTasks [ selectedIndex ] ;
216+ if ( task ) {
217+ document . getElementById ( `task-row-${ task . id } ` ) ?. click ( ) ;
218+ }
219+ }
220+ } ;
221+
222+ window . addEventListener ( 'keydown' , handler , true ) ;
223+ return ( ) => window . removeEventListener ( 'keydown' , handler , true ) ;
224+ } , [ hotkeysEnabled , selectedIndex , currentTasks ] ) ;
225+
185226 useEffect ( ( ) => {
186227 const hashedKey = hashKey ( 'tasksPerPage' , props . email ) ;
187228 const storedTasksPerPage = localStorage . getItem ( hashedKey ) ;
@@ -729,6 +770,73 @@ export const Tasks = (
729770 }
730771 } ;
731772
773+ useHotkeys ( [ 'f' ] , ( ) => {
774+ if ( ! showReports ) {
775+ document . getElementById ( 'search' ) ?. focus ( ) ;
776+ }
777+ } ) ;
778+ useHotkeys ( [ 'a' ] , ( ) => {
779+ if ( ! showReports ) {
780+ document . getElementById ( 'add-new-task' ) ?. click ( ) ;
781+ }
782+ } ) ;
783+ useHotkeys ( [ 'r' ] , ( ) => {
784+ if ( ! showReports ) {
785+ document . getElementById ( 'sync-task' ) ?. click ( ) ;
786+ }
787+ } ) ;
788+ useHotkeys ( [ 'c' ] , ( ) => {
789+ if ( ! showReports && ! _isDialogOpen ) {
790+ const task = currentTasks [ selectedIndex ] ;
791+ if ( ! task ) return ;
792+ // Step 1
793+ const openBtn = document . getElementById ( `task-row-${ task . id } ` ) ;
794+ openBtn ?. click ( ) ;
795+ // Step 2
796+ setTimeout ( ( ) => {
797+ const confirmBtn = document . getElementById (
798+ `mark-task-complete-${ task . id } `
799+ ) ;
800+ confirmBtn ?. click ( ) ;
801+ } , 200 ) ;
802+ } else {
803+ if ( _isDialogOpen ) {
804+ const task = currentTasks [ selectedIndex ] ;
805+ if ( ! task ) return ;
806+ const confirmBtn = document . getElementById (
807+ `mark-task-complete-${ task . id } `
808+ ) ;
809+ confirmBtn ?. click ( ) ;
810+ }
811+ }
812+ } ) ;
813+
814+ useHotkeys ( [ 'd' ] , ( ) => {
815+ if ( ! showReports && ! _isDialogOpen ) {
816+ const task = currentTasks [ selectedIndex ] ;
817+ if ( ! task ) return ;
818+ // Step 1
819+ const openBtn = document . getElementById ( `task-row-${ task . id } ` ) ;
820+ openBtn ?. click ( ) ;
821+ // Step 2
822+ setTimeout ( ( ) => {
823+ const confirmBtn = document . getElementById (
824+ `mark-task-as-deleted-${ task . id } `
825+ ) ;
826+ confirmBtn ?. click ( ) ;
827+ } , 200 ) ;
828+ } else {
829+ if ( _isDialogOpen ) {
830+ const task = currentTasks [ selectedIndex ] ;
831+ if ( ! task ) return ;
832+ const confirmBtn = document . getElementById (
833+ `mark-task-as-deleted-${ task . id } `
834+ ) ;
835+ confirmBtn ?. click ( ) ;
836+ }
837+ }
838+ } ) ;
839+
732840 return (
733841 < section
734842 id = "tasks"
@@ -779,7 +887,11 @@ export const Tasks = (
779887 { showReports ? (
780888 < ReportsView tasks = { tasks } />
781889 ) : (
782- < >
890+ < div
891+ ref = { tableRef }
892+ onMouseEnter = { ( ) => setHotkeysEnabled ( true ) }
893+ onMouseLeave = { ( ) => setHotkeysEnabled ( false ) }
894+ >
783895 { tasks . length != 0 ? (
784896 < >
785897 < 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" >
@@ -793,12 +905,14 @@ export const Tasks = (
793905 </ h3 >
794906 < div className = "hidden sm:flex flex-row w-full items-center gap-2 md:gap-4" >
795907 < Input
908+ id = "search"
796909 type = "text"
797910 placeholder = "Search tasks..."
798911 value = { searchTerm }
799912 onChange = { handleSearchChange }
800913 className = "flex-1 min-w-[150px]"
801914 data-testid = "task-search-bar"
915+ icon = { < Key lable = "f" /> }
802916 />
803917 < MultiSelectFilter
804918 title = "Projects"
@@ -828,10 +942,12 @@ export const Tasks = (
828942 >
829943 < DialogTrigger asChild >
830944 < Button
945+ id = "add-new-task"
831946 variant = "outline"
832947 onClick = { ( ) => setIsAddTaskOpen ( true ) }
833948 >
834949 Add Task
950+ < Key lable = "a" />
835951 </ Button >
836952 </ DialogTrigger >
837953 < DialogContent >
@@ -1013,13 +1129,15 @@ export const Tasks = (
10131129 </ div >
10141130 < div className = "flex flex-col items-end gap-2" >
10151131 < Button
1132+ id = "sync-task"
10161133 variant = "outline"
10171134 onClick = { ( ) => (
10181135 props . setIsLoading ( true ) ,
10191136 syncTasksWithTwAndDb ( )
10201137 ) }
10211138 >
10221139 Sync
1140+ < Key lable = "r" />
10231141 </ Button >
10241142 </ div >
10251143 </ div >
@@ -1079,7 +1197,11 @@ export const Tasks = (
10791197 key = { index }
10801198 >
10811199 < DialogTrigger asChild >
1082- < TableRow key = { index } className = "border-b" >
1200+ < TableRow
1201+ id = { `task-row-${ task . id } ` }
1202+ key = { index }
1203+ className = { `border-b cursor-pointer ${ selectedIndex === index ? 'bg-muted/50' : '' } ` }
1204+ >
10831205 { /* Display task details */ }
10841206 < TableCell className = "py-2" >
10851207 < span
@@ -1842,13 +1964,15 @@ export const Tasks = (
18421964 onClick = { ( ) =>
18431965 handleSaveTags ( task )
18441966 }
1967+ aria-label = "Save tags"
18451968 >
18461969 < CheckIcon className = "h-4 w-4 text-green-500" />
18471970 </ Button >
18481971 < Button
18491972 variant = "ghost"
18501973 size = "icon"
18511974 onClick = { handleCancelTags }
1975+ aria-label = "Cancel editing tags"
18521976 >
18531977 < XIcon className = "h-4 w-4 text-red-500" />
18541978 </ Button >
@@ -2033,7 +2157,11 @@ export const Tasks = (
20332157 { task . status == 'pending' ? (
20342158 < Dialog >
20352159 < DialogTrigger asChild className = "mr-5" >
2036- < Button > Mark As Completed</ Button >
2160+ < Button
2161+ id = { `mark-task-complete-${ task . id } ` }
2162+ >
2163+ Mark As Completed < Key lable = "c" />
2164+ </ Button >
20372165 </ DialogTrigger >
20382166 < DialogContent >
20392167 < DialogTitle >
@@ -2048,14 +2176,15 @@ export const Tasks = (
20482176 < DialogClose asChild >
20492177 < Button
20502178 className = "mr-5"
2051- onClick = { ( ) =>
2179+ onClick = { ( ) => {
20522180 markTaskAsCompleted (
20532181 props . email ,
20542182 props . encryptionSecret ,
20552183 props . UUID ,
20562184 task . uuid
2057- )
2058- }
2185+ ) ;
2186+ setIsDialogOpen ( false ) ;
2187+ } }
20592188 >
20602189 Yes
20612190 </ Button >
@@ -2074,10 +2203,12 @@ export const Tasks = (
20742203 < Dialog >
20752204 < DialogTrigger asChild >
20762205 < Button
2206+ id = { `mark-task-as-deleted-${ task . id } ` }
20772207 className = "mr-4"
20782208 variant = { 'destructive' }
20792209 >
20802210 < Trash2Icon />
2211+ < Key lable = "d" />
20812212 </ Button >
20822213 </ DialogTrigger >
20832214 < DialogContent >
@@ -2093,14 +2224,15 @@ export const Tasks = (
20932224 < DialogClose asChild >
20942225 < Button
20952226 className = "mr-5"
2096- onClick = { ( ) =>
2227+ onClick = { ( ) => {
20972228 markTaskAsDeleted (
20982229 props . email ,
20992230 props . encryptionSecret ,
21002231 props . UUID ,
21012232 task . uuid
2102- )
2103- }
2233+ ) ;
2234+ setIsDialogOpen ( false ) ;
2235+ } }
21042236 >
21052237 Yes
21062238 </ Button >
@@ -2358,7 +2490,7 @@ export const Tasks = (
23582490 </ div >
23592491 </ >
23602492 ) }
2361- </ >
2493+ </ div >
23622494 ) }
23632495 </ section >
23642496 ) ;
0 commit comments