From 681714b52fa80907953b365bf75704379dc5d474 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Wed, 20 Nov 2024 23:45:28 +0200 Subject: [PATCH] [Improvement] Create task and assign another team member (#3353) * implement the assignee dropdown * add the task assignees dropdowm * Update settings.json --------- Co-authored-by: Ruslan Konviser --- .vscode/settings.json | 3 +- apps/web/app/hooks/features/useTaskInput.ts | 12 +- apps/web/app/hooks/features/useTeamTasks.ts | 20 +-- apps/web/app/services/client/api/tasks.ts | 4 +- .../pages/kanban/menu-kanban-card.tsx | 4 +- .../add-task-estimation-hours-modal.tsx | 2 +- apps/web/lib/features/task/task-input.tsx | 149 +++++++++++++++--- 7 files changed, 146 insertions(+), 48 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index be7e65f57..1d7d9e1ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,8 +18,7 @@ "source.fixAll": "explicit", "source.organizeImports": "never", "source.sortMembers": "never", - "organizeImports": "never", - // "source.removeUnusedImports": "always" + "organizeImports": "never" }, "vsicons.presets.angular": true, "deepscan.enable": true, diff --git a/apps/web/app/hooks/features/useTaskInput.ts b/apps/web/app/hooks/features/useTaskInput.ts index d5d803128..4c124edbc 100644 --- a/apps/web/app/hooks/features/useTaskInput.ts +++ b/apps/web/app/hooks/features/useTaskInput.ts @@ -64,6 +64,7 @@ export function useTaskInput({ const taskDescription = useRef(null); const taskLabels = useRef<[] | ITaskLabelsItemList[]>([]); const taskProject = useRef(null); + const taskAssignees = useRef<{id:string}[]>([]); const tasks = customTasks || teamTasks; @@ -134,7 +135,6 @@ export function useTaskInput({ const handleTaskCreation = ({ autoActiveTask = true, autoAssignTaskAuth = true, - assignToUsers = [] }: { autoActiveTask?: boolean; autoAssignTaskAuth?: boolean; @@ -152,19 +152,20 @@ export function useTaskInput({ const statusId = taskStatusList.find( (item) => item.name === taskStatus.current )?.id; + return createTask( { - taskName: query.trim(), + title: query.trim(), issueType: taskIssue || 'Bug', taskStatusId: statusId || (openId as string), status: taskStatus.current || undefined, priority: taskPriority.current || undefined, size: taskSize.current || undefined, tags: taskLabels.current || [], - description: taskDescription.current, - projectId : taskProject.current + description: taskDescription.current ?? '', + projectId : taskProject.current, + members : [...(autoAssignTaskAuth && user?.employee.id ? [{id : user?.employee.id}] : [] ), ...taskAssignees.current] }, - !autoAssignTaskAuth ? assignToUsers : undefined ).then((res) => { setQuery(''); localStorage.setItem('lastTaskIssue', taskIssue || 'Bug'); @@ -237,6 +238,7 @@ export function useTaskInput({ user, userRef, taskProject, + taskAssignees }; } diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index 8b1fa8e9f..ef3ad5307 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -289,7 +289,7 @@ export function useTeamTasks() { const createTask = useCallback( ( { - taskName, + title, issueType, taskStatusId, status = taskStatus[0]?.name, @@ -297,9 +297,10 @@ export function useTeamTasks() { size, tags, description, - projectId + projectId, + members }: { - taskName: string; + title: string; issueType?: string; status?: string; taskStatusId: string; @@ -308,12 +309,13 @@ export function useTeamTasks() { tags?: ITaskLabelsItemList[]; description?: string | null; projectId?: string | null; + members?: { id: string }[] }, - members?: { id: string }[] ) => { + return createQueryCall( { - title: taskName, + title, issueType, status, priority, @@ -322,17 +324,11 @@ export function useTeamTasks() { // 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 - // } - // : {}), projectId, ...(description ? { description: `

${description}

` } : {}), - ...(members ? { members } : {}), + members, taskStatusId: taskStatusId }, - $user.current ).then((res) => { deepCheckAndUpdateTasks(res?.data?.items || [], true); return res; diff --git a/apps/web/app/services/client/api/tasks.ts b/apps/web/app/services/client/api/tasks.ts index 4635b1f43..4e6c4313d 100644 --- a/apps/web/app/services/client/api/tasks.ts +++ b/apps/web/app/services/client/api/tasks.ts @@ -10,7 +10,6 @@ import { getOrganizationIdCookie, getTenantIdCookie } from '@app/helpers'; -import { IUser } from '@app/interfaces'; import { TTasksTimesheetStatisticsParams } from '@app/services/server/requests'; import qs from 'qs'; @@ -108,7 +107,7 @@ export async function updateTaskAPI(taskId: string, body: Partial) { return put>(`/tasks/${taskId}`, body); } -export async function createTeamTaskAPI(body: Partial & { title: string }, user: IUser | undefined) { +export async function createTeamTaskAPI(body: Partial & { title: string }) { if (GAUZY_API_BASE_SERVER_URL.value) { const organizationId = getOrganizationIdCookie(); const teamId = getActiveTeamIdCookie(); @@ -118,7 +117,6 @@ export async function createTeamTaskAPI(body: Partial & { title: st const datas: ICreateTask = { description: '', - members: user?.employee?.id ? [{ id: user.employee.id }] : [], teams: [ { id: teamId diff --git a/apps/web/components/pages/kanban/menu-kanban-card.tsx b/apps/web/components/pages/kanban/menu-kanban-card.tsx index 26ebbc833..4d9bb806c 100644 --- a/apps/web/components/pages/kanban/menu-kanban-card.tsx +++ b/apps/web/components/pages/kanban/menu-kanban-card.tsx @@ -82,7 +82,7 @@ export default function MenuKanbanCard({ item: task, member }: { item: ITeamTask await createTask({ ...task, taskStatusId: task.taskStatusId ?? taskStatus[0].id, - taskName: `Copy ${task.title}`, + title: `Copy ${task.title}`, issueType: task.issueType ?? 'Bug' }); } catch (error) { @@ -201,7 +201,7 @@ interface ITeamMemberSelectProps { * * @return {JSX.Element} The multi select component */ -function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element { +export function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element { const { teamMembers, task } = props; const t = useTranslations(); 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 22880e1f6..c77d45c2b 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 @@ -591,7 +591,7 @@ export function SearchTaskInput(props: ISearchTaskInputProps) { setCreateTaskLoading(true); if (taskName.trim().length < 5) return; await createTask({ - taskName: taskName.trim(), + title: taskName.trim(), status: taskStatus[0].name, taskStatusId: taskStatus[0].id, issueType: 'Bug' // TODO: Let the user choose the issue type diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx index 31d551d49..f63ad9ed9 100644 --- a/apps/web/lib/features/task/task-input.tsx +++ b/apps/web/lib/features/task/task-input.tsx @@ -20,12 +20,13 @@ import { ITaskSize, ITaskStatus, ITeamTask, - Nullable + Nullable, + OT_Member } from '@app/interfaces'; import { activeTeamTaskId, timerStatusState } from '@app/stores'; import { clsxm } from '@app/utils'; -import { Popover, Transition } from '@headlessui/react'; -import { PlusIcon } from '@heroicons/react/20/solid'; +import { Combobox, Popover, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronUpDownIcon, PlusIcon } from '@heroicons/react/20/solid'; import { Button, Card, @@ -37,6 +38,7 @@ import { } from 'lib/components'; import { CheckCircleTickIcon as TickCircleIcon } from 'assets/svg'; import { + Fragment, MutableRefObject, PropsWithChildren, useCallback, @@ -86,7 +88,6 @@ type Props = { onTaskCreated?: (task: ITeamTask | undefined) => void; cardWithoutShadow?: boolean; assignTaskPopup?: boolean; - forParentChildRelationship?: boolean; } & PropsWithChildren; @@ -107,7 +108,7 @@ export function TaskInput(props: Props) { const { viewType = 'input-trigger', showTaskNumber = false, - showCombobox = true + showCombobox = true, } = props; const datas = useTaskInput({ @@ -449,22 +450,18 @@ export function TaskInput(props: Props) { ); const taskCard = ( - + ); return viewType === 'one-view' ? ( @@ -518,7 +515,7 @@ function TaskCard({ cardWithoutShadow, forParentChildRelationship, updatedTaskList, - assignTaskPopup + assignTaskPopup, }: { datas: Partial; onItemClick?: (task: ITeamTask) => void; @@ -535,6 +532,7 @@ function TaskCard({ const t = useTranslations(); const activeTaskEl = useRef(null); const { taskLabels: taskLabelsData } = useTaskLabels(); + const { activeTeam } = useOrganizationTeams(); const { taskStatus, @@ -542,7 +540,8 @@ function TaskCard({ taskSize, taskLabels, taskDescription, - taskProject + taskProject, + taskAssignees } = datas; const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5); @@ -643,6 +642,9 @@ function TaskCard({ task={datas.inputTask} /> + {taskAssignees !== undefined && } + + ); } + + +/** + * ---------------------------------------------- + * ----------- ASSIGNEES MULTI SELECT ----------- + * ---------------------------------------------- + */ + +interface ITeamMemberSelectProps { + teamMembers: OT_Member[]; + assignees : MutableRefObject<{ + id: string; + }[]> +} +/** + * A multi select component for assignees + * + * @param {object} props - The props object + * @param {string[]} props.teamMembers - Members of the current team + * @param {ITeamMemberSelectProps["assignees"]} props.assignees - Assigned members + * + * @return {JSX.Element} The multi select component + */ + function AssigneesSelect(props: ITeamMemberSelectProps): JSX.Element { + const { teamMembers , assignees} = props; + const t = useTranslations(); + const {user} = useAuthenticateUser() + const authMember = useMemo(() => teamMembers.find(member => member.employee.user?.id == user?.id), [teamMembers, user?.id]) + + return ( +
+ +
+
+ + + {t('common.ASSIGNEE')} + +
+ + + {authMember && ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary/5' : 'text-gray-900' + }` + } + value={authMember} + > + + + {authMember.employee.fullName} + + )} + + {teamMembers + .filter((member) => member.employee.user?.id != user?.id) + .map((member) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary/5' : 'text-gray-900' + }` + } + onClick={() => { + const isAssigned = assignees.current.map(el => el.id).includes(member.employee.id); + + + if (isAssigned) { + assignees.current = assignees.current.filter((el) => el.id != member.employee.id) + } else { + assignees.current = [...assignees.current, {id:member.employee.id}]; + } + }} + value={member} + > + {assignees.current.map(el => el.id).includes(member.employee.id) && ( + + + )} + + {member.employee.fullName} + + ))} + + +
+
+
+ ); +}