From 46aae43aa513d2fe46afc07c862486de1f9a6c89 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Thu, 5 Sep 2024 19:49:13 +0200 Subject: [PATCH] [Feat] Planned tasks popup | search / add / create task (#2979) * feat: add search / create action on Planned tasks | task card * feat: add the search task list * feat: keep open the popover panel when the input is focused on --- .../add-task-estimation-hours-modal.tsx | 400 ++++++++++++++---- 1 file changed, 307 insertions(+), 93 deletions(-) diff --git a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx index 760a52ff7..dcd2761b5 100644 --- a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx +++ b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TASKS_ESTIMATE_HOURS_MODAL_DATE } from '@app/constants'; -import { useMemo, useCallback, useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { useMemo, useCallback, useState, useEffect, useRef, Dispatch, SetStateAction } from 'react'; import { PiWarningCircleFill } from 'react-icons/pi'; -import { Card, InputField, Modal, SpinnerLoader, Text, VerticalSeparator } from 'lib/components'; +import { Card, InputField, Modal, SpinnerLoader, Text, Tooltip, VerticalSeparator } from 'lib/components'; import { Button } from '@components/ui/button'; import { useTranslations } from 'next-intl'; -import { useAuthenticateUser, useDailyPlan, useModal, useTeamTasks, useTimerView } from '@app/hooks'; +import { useAuthenticateUser, useDailyPlan, useModal, useTaskStatus, useTeamTasks, useTimerView } from '@app/hooks'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { TaskEstimate } from '../task/task-estimate'; import { IDailyPlan, ITeamTask } from '@app/interfaces'; @@ -18,6 +18,8 @@ import { DEFAULT_PLANNED_TASK_ID } from '@app/constants'; import { ActiveTaskHandlerModal } from './active-task-handler-modal'; import { TaskDetailsModal } from './task-details-modal'; import { Popover, Transition } from '@headlessui/react'; +import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; +import { Cross2Icon } from '@radix-ui/react-icons'; interface IAddTasksEstimationHoursModalProps { closeModal: () => void; @@ -38,7 +40,7 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa const { updateDailyPlan, myDailyPlans } = useDailyPlan(); const { startTimer } = useTimerView(); const { activeTeam, activeTeamTask, setActiveTask } = useTeamTasks(); - + const [showSearchInput, setShowSearchInput] = useState(false); const [workTimePlanned, setworkTimePlanned] = useState(plan.workTimePlanned); const currentDate = useMemo(() => new Date().toISOString().split('T')[0], []); const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); @@ -161,103 +163,125 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); + }, [isOpen, tasks]); return ( <> - -
-
+ +
+
{t('timer.todayPlanSettings.TITLE')} -
- - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')}{' '} - * - -
- setworkTimePlanned(parseFloat(e.target.value))} - required - noWrapper - min={0} - value={workTimePlanned} - defaultValue={plan.workTimePlanned ?? 0} - /> -
- + {showSearchInput ? ( + + ) : ( +
+ + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')}{' '} + * + +
+ setworkTimePlanned(parseFloat(e.target.value))} + required + noWrapper + min={0} + value={workTimePlanned} + defaultValue={plan.workTimePlanned ?? 0} + /> +
-
+ )} +
-
-
- {t('task.TITLE_PLURAL')} - * +
+
+
+ {t('task.TITLE_PLURAL')} + * +
+
+ {t('dailyPlan.TOTAL_ESTIMATED')} : + + {formatIntegerToHour(tasksEstimationTimes)} + +
-
- {t('dailyPlan.TOTAL_ESTIMATED')} : - - {formatIntegerToHour(tasksEstimationTimes)} - +
+ +
    + {sortedTasks.map((task, index) => ( + + ))} +
+ +
-
- {sortedTasks.map((task, index) => ( - - ))} +
+ {warning && ( + <> + + {warning} + + )}
-
- {warning && ( - <> - - {warning} - +
+
+ +
-
- - -
@@ -272,15 +296,182 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa ); } +/** + * ---------------------------------------------------------------- + * --------- Search / Add / Create task input ----------- + * ---------------------------------------------------------------- + */ + +interface ISearchTaskInputProps { + selectedPlan: IDailyPlan; + setShowSearchInput: Dispatch>; + setDefaultTask: Dispatch>; + defaultTask: ITeamTask | null; +} + +/** + * Search task input + * + * @param {Object} props - The props object + * @param {string} props.selectedPlan - The selected plan + * @param {Dispatch>} props.setShowSearchInput - A setter for (showing / hiding) the input + * @param {Dispatch>} props.setDefaultTask - A function that sets default planned task + * @param {ITeamTask} props.defaultTask - The default planned task + * + * @returns The Search input component + */ +function SearchTaskInput(props: ISearchTaskInputProps) { + const { selectedPlan, setShowSearchInput, defaultTask, setDefaultTask } = props; + const { tasks: teamTasks, createTask } = useTeamTasks(); + const { taskStatus } = useTaskStatus(); + const [taskName, setTaskName] = useState(''); + const [tasks, setTasks] = useState([]); + const [createTaskLoading, setCreateTaskLoading] = useState(false); + const [isSearchInputFocused, setIsSearchInputFocused] = useState(false); + const t = useTranslations(); + + // The ref for the popover button (rendered as an input) + const searchInputRef = useRef(null); + + const isTaskPlanned = useCallback( + (taskId: string) => { + return selectedPlan?.tasks?.some((task) => task.id == taskId); + }, + [selectedPlan.tasks] + ); + + useEffect(() => { + setTasks( + teamTasks + .filter((task) => task.title.toLowerCase().includes(taskName.toLowerCase())) + // Put the unplanned tasks at the top of the list. + .sort((task1, task2) => { + if (isTaskPlanned(task1.id) && !isTaskPlanned(task2.id)) { + return 1; + } else if (!isTaskPlanned(task1.id) && isTaskPlanned(task2.id)) { + return -1; + } else { + return 0; + } + }) + ); + }, [isTaskPlanned, selectedPlan.tasks, taskName, teamTasks]); + + const handleCreateTask = useCallback(async () => { + try { + setCreateTaskLoading(true); + if (taskName.trim().length < 5) return; + await createTask({ + taskName: taskName.trim(), + status: taskStatus[0].name, + taskStatusId: taskStatus[0].id, + issueType: 'Bug' // TODO: Let the user choose the issue type + }); + } catch (error) { + console.log(error); + } finally { + setCreateTaskLoading(false); + } + }, [createTask, taskName, taskStatus]); + + /** + * Focus on the search input when the popover is mounted. + */ + useEffect(() => { + searchInputRef.current?.focus(); + }, []); + + return ( + +
+ Select or create task for the plan +
+ setTaskName(e.target.value)} + onFocus={() => setIsSearchInputFocused(true)} + value={taskName} + /> + +
+
+ + + {tasks.length ? ( + + +
    + {tasks.map((task, index) => ( +
  • + +
  • + ))} +
+ +
+
+ ) : ( + + + + )} +
+
+ ); +} + +/** + * ---------------------------------------------------------------- + * -------------------- TASK CARD ----------------------- + * ---------------------------------------------------------------- + */ + interface ITaskCardProps { task: ITeamTask; setDefaultTask: Dispatch>; isDefaultTask: boolean; plan: IDailyPlan; + viewListMode?: 'planned' | 'searched'; } -function TaskCard({ task, plan, isDefaultTask, setDefaultTask }: ITaskCardProps) { +function TaskCard(props: ITaskCardProps) { + const { task, plan, viewListMode = 'planned', isDefaultTask, setDefaultTask } = props; const { getTaskById } = useTeamTasks(); + const { addTaskToPlan } = useDailyPlan(); + const [addToPlanLoading, setAddToPlanLoading] = useState(false); const { isOpen: isTaskDetailsModalOpen, closeModal: closeTaskDetailsModal, @@ -295,6 +486,21 @@ function TaskCard({ task, plan, isDefaultTask, setDefaultTask }: ITaskCardProps) const t = useTranslations(); + /** + * The function that adds the task to the selected plan + */ + const handleAddTask = useCallback(async () => { + try { + setAddToPlanLoading(true); + + if (plan.id) await addTaskToPlan({ taskId: task.id }, plan.id); + } catch (error) { + console.log(error); + } finally { + setAddToPlanLoading(false); + } + }, [addTaskToPlan, plan.id, task.id]); + return (
-
- {t('dailyPlan.ESTIMATED')} : -
- - - + {viewListMode === 'searched' ? ( + + ) : ( + <> +
+ {t('dailyPlan.ESTIMATED')} : +
+ + + + + )}