From bc6384930077a326301db21f9cd241d241a6728a Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Tue, 19 Nov 2024 22:35:23 +0200 Subject: [PATCH] [Feature] Task | Add project information in the task (#3350) * add project in task details page / possiblity to edit * set the project when creating task --- apps/web/app/hooks/features/useTaskInput.ts | 7 +- apps/web/app/hooks/features/useTeamTasks.ts | 16 +- .../blocks/task-secondary-info.tsx | 369 +++++++++++------- apps/web/components/ui/svgs/project-icon.tsx | 18 + .../web/lib/features/auth-user-task-input.tsx | 18 +- apps/web/lib/features/task/task-input.tsx | 18 +- 6 files changed, 291 insertions(+), 155 deletions(-) create mode 100644 apps/web/components/ui/svgs/project-icon.tsx diff --git a/apps/web/app/hooks/features/useTaskInput.ts b/apps/web/app/hooks/features/useTaskInput.ts index fe21f5716..d5d803128 100644 --- a/apps/web/app/hooks/features/useTaskInput.ts +++ b/apps/web/app/hooks/features/useTaskInput.ts @@ -63,6 +63,7 @@ export function useTaskInput({ const taskSize = useRef(null); const taskDescription = useRef(null); const taskLabels = useRef<[] | ITaskLabelsItemList[]>([]); + const taskProject = useRef(null); const tasks = customTasks || teamTasks; @@ -160,7 +161,8 @@ export function useTaskInput({ priority: taskPriority.current || undefined, size: taskSize.current || undefined, tags: taskLabels.current || [], - description: taskDescription.current + description: taskDescription.current, + projectId : taskProject.current }, !autoAssignTaskAuth ? assignToUsers : undefined ).then((res) => { @@ -233,7 +235,8 @@ export function useTaskInput({ taskLabels, taskDescription, user, - userRef + userRef, + taskProject, }; } diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index a85217c6c..8b1fa8e9f 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -296,7 +296,8 @@ export function useTeamTasks() { priority, size, tags, - description + description, + projectId }: { taskName: string; issueType?: string; @@ -306,6 +307,7 @@ export function useTeamTasks() { size?: string; tags?: ITaskLabelsItemList[]; description?: string | null; + projectId?: string | null; }, members?: { id: string }[] ) => { @@ -319,11 +321,13 @@ export function useTeamTasks() { tags, // Set Project Id to cookie // TODO: Make it dynamic when we add Dropdown in Navbar - ...(activeTeam?.projects && activeTeam?.projects.length > 0 - ? { - projectId: activeTeam.projects[0].id - } - : {}), + + // ...(activeTeam?.projects && activeTeam?.projects.length > 0 + // ? { + // projectId: activeTeam.projects[0].id + // } + // : {}), + projectId, ...(description ? { description: `

${description}

` } : {}), ...(members ? { members } : {}), taskStatusId: taskStatusId diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx index 88c8690fd..959b2c4da 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx @@ -4,20 +4,16 @@ import { detailedTaskState } from '@app/stores'; import { PlusIcon } from '@heroicons/react/20/solid'; import { Button, Card, Modal, SpinnerLoader, Tooltip } from 'lib/components'; import { - ActiveTaskPropertiesDropdown, - ActiveTaskSizesDropdown, - ActiveTaskStatusDropdown, - ActiveTaskVersionDropdown, - EpicPropertiesDropdown as TaskEpicDropdown, - TaskLabels, - TaskStatus, - useTaskLabelsValue + ActiveTaskPropertiesDropdown, + ActiveTaskSizesDropdown, + ActiveTaskStatusDropdown, + ActiveTaskVersionDropdown, + EpicPropertiesDropdown as TaskEpicDropdown, + TaskLabels, + TaskStatus, + useTaskLabelsValue } from 'lib/features'; -import { - TaskPrioritiesForm, - TaskSizesForm, - TaskStatusesForm -} from 'lib/settings'; +import { TaskPrioritiesForm, TaskSizesForm, TaskStatusesForm } from 'lib/settings'; import { VersionForm } from 'lib/settings/version-form'; import { cloneDeep } from 'lodash'; import Link from 'next/link'; @@ -29,69 +25,65 @@ import { ChevronDownIcon, Square4OutlineIcon } from 'assets/svg'; import { Listbox, Transition } from '@headlessui/react'; import { clsxm } from '@/app/utils'; import { organizationProjectsState } from '@/app/stores/organization-projects'; +import ProjectIcon from '@components/ui/svgs/project-icon'; type StatusType = 'version' | 'epic' | 'status' | 'label' | 'size' | 'priority'; const TaskSecondaryInfo = () => { - const task = useAtomValue(detailedTaskState); - const { updateTask } = useTeamTasks(); - - const { handleStatusUpdate } = useTeamTasks(); - - const t = useTranslations(); - - const modal = useModal(); - const [formTarget, setFormTarget] = useState(null); - - const openModalEditionHandle = useCallback( - (type: StatusType) => { - return () => { - setFormTarget(type); - modal.openModal(); - }; - }, - [modal] - ); - - const onVersionCreated = useCallback( - (version: ITaskVersionCreate) => { - handleStatusUpdate( - version.value || version.name, - 'version', - task?.taskStatusId, - task - ); - }, - [task, handleStatusUpdate] - ); - - const onTaskSelect = useCallback( - async (parentTask: ITeamTask | undefined) => { - if (!parentTask) return; - const childTask = cloneDeep(task); - - await updateTask({ - ...childTask, - parentId: parentTask.id ? parentTask.id : null, - parent: parentTask.id ? parentTask : null - } as any); - }, - [task, updateTask] - ); - - const taskLabels = useTaskLabelsValue(); - - const tags = useMemo(() => { - return ( - task?.tags - .map((tag) => { - return taskLabels[tag.name]; - }) - .filter(Boolean) || [] - ); - }, [taskLabels, task?.tags]); - - return ( + const task = useAtomValue(detailedTaskState); + const { updateTask } = useTeamTasks(); + + const { handleStatusUpdate } = useTeamTasks(); + + const t = useTranslations(); + + const modal = useModal(); + const [formTarget, setFormTarget] = useState(null); + + const openModalEditionHandle = useCallback( + (type: StatusType) => { + return () => { + setFormTarget(type); + modal.openModal(); + }; + }, + [modal] + ); + + const onVersionCreated = useCallback( + (version: ITaskVersionCreate) => { + handleStatusUpdate(version.value || version.name, 'version', task?.taskStatusId, task); + }, + [task, handleStatusUpdate] + ); + + const onTaskSelect = useCallback( + async (parentTask: ITeamTask | undefined) => { + if (!parentTask) return; + const childTask = cloneDeep(task); + + await updateTask({ + ...childTask, + parentId: parentTask.id ? parentTask.id : null, + parent: parentTask.id ? parentTask : null + } as any); + }, + [task, updateTask] + ); + + const taskLabels = useTaskLabelsValue(); + + const tags = useMemo(() => { + return ( + task?.tags + .map((tag) => { + return taskLabels[tag.name]; + }) + .filter(Boolean) || [] + ); + }, [taskLabels, task?.tags]); + + return (
{/* Version */} @@ -112,6 +104,23 @@ const TaskSecondaryInfo = () => { + {/* Epic */} + {task && task.issueType === 'Story' && ( + + { + onTaskSelect({ + id: d + } as ITeamTask); + }} + className="lg:min-w-[170px] text-black" + forDetails={true} + sidebarUI={true} + taskStatusClassName="text-[0.625rem] w-[7.6875rem] h-[2.35rem] max-w-[7.6875rem] rounded 3xl:text-xs" + defaultValue={task.parentId || ''} + /> + + )} {/* Epic */} {task && task.issueType === 'Story' && ( @@ -130,8 +139,27 @@ const TaskSecondaryInfo = () => { )} + {task && } {task && } + {/* Task Status */} + + + + + {/* Task Status */} { + {/* Task Labels */} + + + + {tags.length > 0 && ( + +
+ {tags.map((tag, i) => { + return ( + + + + ); + })} +
+
+ )} {/* Task Labels */} {
- ); + ); }; const EpicParent = ({ task }: { task: ITeamTask }) => { - const t = useTranslations(); - - if (task?.issueType === 'Story') { - return <>; - } - - return (!task?.issueType || - task?.issueType === 'Task' || - task?.issueType === 'Bug') && - task?.rootEpic ? ( - - - -
-
- -
-
{`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`}
-
- -
-
- ) : ( - <> - ); + const t = useTranslations(); + + if (task?.issueType === 'Story') { + return <>; + } + + return (!task?.issueType || task?.issueType === 'Task' || task?.issueType === 'Bug') && task?.rootEpic ? ( + + + +
+
+ +
+
{`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`}
+
+ +
+
+ ) : ( + <> + ); }; - - interface ITaskProjectDropdownProps { - task: ITeamTask; + task?: ITeamTask; + controlled?: boolean; + onChange?: (project: IProject) => void; + styles?: { + container?: string; // The dropdown element + value?: string; + listCard?: string; // The listbox + }; } export default TaskSecondaryInfo; @@ -282,50 +337,58 @@ export default TaskSecondaryInfo; * * @param {Object} props - The props object * @param {ITeamTask} props.task - The ITeamTask object which + * @param {boolean} props.controlled - If [true], changes are managed by external handlers (i.e :props.onChange) + * @param {(project: IProject) => void} props.onChange - The function called when user selects a value (external handler) * * @returns {JSX.Element} - The Dropdown element */ -function ProjectDropDown (props : ITaskProjectDropdownProps) { - - const {task} = props +export function ProjectDropDown(props: ITaskProjectDropdownProps) { + const { task, controlled = false, onChange, styles } = props; - const organizationProjects = useAtomValue(organizationProjectsState) - const {getOrganizationProjects} = useOrganizationProjects() - const {updateTask, updateLoading} = useTeamTasks() - const t = useTranslations() + const organizationProjects = useAtomValue(organizationProjectsState); + const { getOrganizationProjects } = useOrganizationProjects(); + const { updateTask, updateLoading } = useTeamTasks(); + const t = useTranslations(); useEffect(() => { - getOrganizationProjects() - },[getOrganizationProjects]) - + getOrganizationProjects(); + }, [getOrganizationProjects]); const [selected, setSelected] = useState(); // Set the task project if any useEffect(() => { - setSelected(organizationProjects.find(project => { - return project.id === task.projectId - })) - },[organizationProjects, task.projectId]) - - // Update the project - const handleUpdateProject = useCallback(async (project : IProject) => { - try { - await updateTask({ ...task, projectId: project.id }); - - setSelected(project); - } catch (error) { - console.error(error); + if (task) { + setSelected( + organizationProjects.find((project) => { + return project.id == task.projectId; + }) + ); } - },[task, updateTask]) + }, [organizationProjects, task, task?.projectId]); + // Update the project + const handleUpdateProject = useCallback( + async (project: IProject) => { + try { + if (task) { + await updateTask({ ...task, projectId: project.id }); + } + } catch (error) { + console.error(error); + } + }, + [task, updateTask] + ); // Remove the project const handleRemoveProject = useCallback(async () => { try { - await updateTask({ ...task, projectId: null }); + if (task) { + await updateTask({ ...task, projectId: null }); - setSelected(undefined); + setSelected(undefined); + } } catch (error) { console.error(error); } @@ -334,25 +397,42 @@ function ProjectDropDown (props : ITaskProjectDropdownProps) { return (
{ + if (controlled && onChange) { + onChange(project); + } else { + handleUpdateProject(project); + } + + setSelected(project); + }} > {({ open }) => { return ( <> + {selected && ( +
+ +
+ )} {updateLoading ? ( ) : ( -

{selected?.name ?? 'Project'}

+

+ {selected?.name ?? 'Project'} +

)} {organizationProjects.map((item, i) => { return ( @@ -386,13 +469,15 @@ function ProjectDropDown (props : ITaskProjectDropdownProps) { ); })} - + {!controlled && ( + + )} diff --git a/apps/web/components/ui/svgs/project-icon.tsx b/apps/web/components/ui/svgs/project-icon.tsx new file mode 100644 index 000000000..e15ec9d40 --- /dev/null +++ b/apps/web/components/ui/svgs/project-icon.tsx @@ -0,0 +1,18 @@ +export default function ProjectIcon() { + return ( + + + + + + ); +} diff --git a/apps/web/lib/features/auth-user-task-input.tsx b/apps/web/lib/features/auth-user-task-input.tsx index 33a6452ad..949392eac 100644 --- a/apps/web/lib/features/auth-user-task-input.tsx +++ b/apps/web/lib/features/auth-user-task-input.tsx @@ -9,6 +9,7 @@ import { TaskInput } from './task/task-input'; import { TaskLabels } from './task/task-labels'; import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown, ActiveTaskStatusDropdown } from './task/task-status'; import { useTranslations } from 'next-intl'; +import { ProjectDropDown } from '@components/pages/task/details-section/blocks/task-secondary-info'; export function AuthUserTaskInput({ className }: IClassName) { const t = useTranslations(); @@ -31,29 +32,38 @@ export function AuthUserTaskInput({ className }: IClassName) {
+ {activeTeamTask && ( + + )}
{/*
diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx index 1ae8b0945..31d551d49 100644 --- a/apps/web/lib/features/task/task-input.tsx +++ b/apps/web/lib/features/task/task-input.tsx @@ -58,6 +58,7 @@ import { useTranslations } from 'next-intl'; import { useInfinityScrolling } from '@app/hooks/useInfinityFetch'; import { ObserverComponent } from '@components/shared/Observer'; import { LazyRender } from 'lib/components/lazy-render'; +import { ProjectDropDown } from '@components/pages/task/details-section/blocks/task-secondary-info'; type Props = { task?: Nullable; @@ -540,7 +541,8 @@ function TaskCard({ taskPriority, taskSize, taskLabels, - taskDescription + taskDescription, + taskProject } = datas; const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5); @@ -640,6 +642,20 @@ function TaskCard({ }} task={datas.inputTask} /> + + { + if (taskProject) { + taskProject.current = project.id + } + }} + /> +
)}