From d9871493832f393dbb105196742594b1646eb6ef Mon Sep 17 00:00:00 2001 From: Anish Date: Sun, 10 Mar 2024 02:32:52 +0500 Subject: [PATCH] 2277 feature modifications in kanban 2 (#2290) * fix(kanban): working on kanban section * [Fix]: added the kanban create task * [Fix]: added the kanban create task * [Fix]: added the kanban create task * [Fix]: added the kanban create task * [Fix]: added the kanban create task --- apps/web/app/[locale]/kanban/page.tsx | 2 +- apps/web/app/hooks/features/useKanban.ts | 2 +- apps/web/app/hooks/features/useTeamTasks.ts | 2 +- .../pages/kanban/create-task-modal.tsx | 23 + apps/web/lib/components/Kanban.tsx | 20 +- apps/web/lib/components/image-overlapper.tsx | 6 +- apps/web/lib/components/kanban-card.tsx | 7 +- .../lib/features/task/task-input-kanban.tsx | 482 ++++++++++++++++++ 8 files changed, 527 insertions(+), 17 deletions(-) create mode 100644 apps/web/components/pages/kanban/create-task-modal.tsx create mode 100644 apps/web/lib/features/task/task-input-kanban.tsx diff --git a/apps/web/app/[locale]/kanban/page.tsx b/apps/web/app/[locale]/kanban/page.tsx index 0310c07e5..2029d6e0c 100644 --- a/apps/web/app/[locale]/kanban/page.tsx +++ b/apps/web/app/[locale]/kanban/page.tsx @@ -21,7 +21,7 @@ import { InviteFormModal } from 'lib/features/team/invite/invite-form-modal'; import { userTimezone } from '@app/helpers'; const Kanban = () => { - const { data } = useKanban(); + const { data, } = useKanban(); const { activeTeam } = useOrganizationTeams(); const t = useTranslations(); diff --git a/apps/web/app/hooks/features/useKanban.ts b/apps/web/app/hooks/features/useKanban.ts index c38e4a465..0d1434435 100644 --- a/apps/web/app/hooks/features/useKanban.ts +++ b/apps/web/app/hooks/features/useKanban.ts @@ -10,7 +10,7 @@ export function useKanban() { const [loading, setLoading] = useState(true); const [kanbanBoard, setKanbanBoard] = useRecoilState(kanbanBoardState); const taskStatusHook = useTaskStatus(); - const { tasks, tasksFetching, updateTask } = useTeamTasks(); + const { tasks, tasksFetching, updateTask, } = useTeamTasks(); /** * format data for kanban board */ diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index 238fe0fa6..2b83ec20f 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -57,7 +57,7 @@ export function useTeamTasks() { const { firstLoad, firstLoadData: firstLoadTasksData } = useFirstLoad(); // Queries hooks - const { queryCall, loading, loadingRef } = useQuery(getTeamTasksAPI); + const { queryCall, loading, loadingRef, } = useQuery(getTeamTasksAPI); const { queryCall: getTasksByIdQueryCall, loading: getTasksByIdLoading } = useQuery(getTasksByIdAPI); const { queryCall: getTasksByEmployeeIdQueryCall, loading: getTasksByEmployeeIdLoading } = useQuery(getTasksByEmployeeIdAPI); diff --git a/apps/web/components/pages/kanban/create-task-modal.tsx b/apps/web/components/pages/kanban/create-task-modal.tsx new file mode 100644 index 000000000..a7333efb1 --- /dev/null +++ b/apps/web/components/pages/kanban/create-task-modal.tsx @@ -0,0 +1,23 @@ +import { TaskInputKanban } from 'lib/features/task/task-input-kanban'; +import React from 'react'; + +const CreateTaskModal = (props: { task: any; initEditMode: boolean; tasks: any; title: string }) => { + return ( +
+ { + console.log(e); + }} + /> +
+ ); +}; + +export default CreateTaskModal; diff --git a/apps/web/lib/components/Kanban.tsx b/apps/web/lib/components/Kanban.tsx index aa8f24c4a..a53d588df 100644 --- a/apps/web/lib/components/Kanban.tsx +++ b/apps/web/lib/components/Kanban.tsx @@ -19,6 +19,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover' import { Button } from '@components/ui/button'; import { useTranslations } from 'next-intl'; import { AddIcon } from 'assets/svg'; +import { useModal } from '@app/hooks'; +import { Modal } from './modal'; +import CreateTaskModal from '@components/pages/kanban/create-task-modal'; const grid = 8; @@ -135,9 +138,6 @@ export const KanbanDroppable = ({ {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => (
@@ -371,6 +372,9 @@ const KanbanDraggable = ({ )} )} + + + ); }; diff --git a/apps/web/lib/components/image-overlapper.tsx b/apps/web/lib/components/image-overlapper.tsx index 08dfec156..494a64e6e 100644 --- a/apps/web/lib/components/image-overlapper.tsx +++ b/apps/web/lib/components/image-overlapper.tsx @@ -42,7 +42,11 @@ export default function ImageOverlapper({ className="absolute hover:!z-20 transition-all hover:scale-110" style={{ zIndex: index + 1, left: index * 30, top: isMoreThanDisplay ? -8 : -16 }} > - + {`${image.alt}
@@ -266,7 +263,7 @@ export default function Item(props: ItemProps) {
{item.issueType && ( -
+
; + tasks?: ITeamTask[]; + kanbanTitle?: string; + onTaskClick?: (task: ITeamTask) => void; + initEditMode?: boolean; + onCloseCombobox?: () => void; + inputLoader?: boolean; + onEnterKey?: (taskName: string, task: ITeamTask) => void; + keepOpen?: boolean; + loadingRef?: MutableRefObject; + closeable_fc?: () => void; + viewType?: 'input-trigger' | 'one-view'; + createOnEnterClick?: boolean; + showTaskNumber?: boolean; + showCombobox?: boolean; + autoAssignTaskAuth?: boolean; + fullWidthCombobox?: boolean; + fullHeightCombobox?: boolean; + placeholder?: string; + autoFocus?: boolean; + autoInputSelectText?: boolean; + usersTaskCreatedAssignTo?: { id: string }[]; + onTaskCreated?: (task: ITeamTask | undefined) => void; + cardWithoutShadow?: boolean; + + forParentChildRelationship?: boolean; +} & PropsWithChildren; + +/** + * If task passed then some function should not considered as global state + * + * @param param0 + * @returns + */ + +export function TaskInputKanban(props: Props) { + const t = useTranslations(); + + const { viewType = 'input-trigger', showTaskNumber = false } = props; + + const datas = useTaskInput({ + task: props.task, + initEditMode: props.initEditMode, + tasks: props.tasks + }); + + const { updateOrganizationTeamEmployee } = useOrganizationEmployeeTeams(); + const { activeTeam } = useOrganizationTeams(); + const { user } = useAuthenticateUser(); + + const onCloseComboboxRef = useCallbackRef(props.onCloseCombobox); + const closeable_fcRef = useCallbackRef(props.closeable_fc); + const $onTaskClick = useCallbackRef(props.onTaskClick); + const $onTaskCreated = useCallbackRef(props.onTaskCreated); + const inputRef = useRef(null); + const timerStatus = useRecoilValue(timerStatusState); + const timerRunningStatus = useMemo(() => { + return Boolean(timerStatus?.running); + }, [timerStatus]); + + const onTaskCreated = useCallback( + (task: ITeamTask | undefined) => $onTaskCreated.current && $onTaskCreated.current(task), + [$onTaskCreated] + ); + + const onTaskClick = useCallback( + (task: ITeamTask) => $onTaskClick.current && $onTaskClick.current(task), + [$onTaskClick] + ); + + const { inputTask, editMode, setEditMode, setQuery, updateLoading, updateTaskTitleHandler, setFilter, taskIssue } = + datas; + + const inputTaskTitle = useMemo(() => inputTask?.title || '', [inputTask?.title]); + + const [taskName, setTaskName] = useState(''); + + const { targetEl, ignoreElementRef } = useOutsideClick( + () => !props.keepOpen && setEditMode(false) + ); + + useEffect(() => { + setQuery(taskName === inputTask?.title ? '' : taskName); + }, [taskName, inputTask, setQuery]); + + useEffect(() => { + setTaskName(inputTaskTitle); + }, [editMode, inputTaskTitle]); + + useEffect(() => { + /** + * Call onCloseCombobox only when the menu has been closed + */ + !editMode && onCloseComboboxRef.current && onCloseComboboxRef.current(); + }, [editMode, onCloseComboboxRef]); + + /** + * set the active task for the authenticated user + */ + const setAuthActiveTask = useCallback( + (task: ITeamTask) => { + if (datas.setActiveTask) { + datas.setActiveTask(task); + + // Update Current user's active task to sync across multiple devices + const currentEmployeeDetails = activeTeam?.members.find( + (member) => member.employeeId === user?.employee?.id + ); + if (currentEmployeeDetails && currentEmployeeDetails.id) { + updateOrganizationTeamEmployee(currentEmployeeDetails.id, { + organizationId: task.organizationId, + activeTaskId: task.id, + organizationTeamId: activeTeam?.id, + tenantId: activeTeam?.tenantId + }); + } + } + setEditMode(false); + }, + [datas, setEditMode, activeTeam, user, updateOrganizationTeamEmployee] + ); + + /** + * On update task name + */ + const updateTaskNameHandler = useCallback( + (task: ITeamTask, title: string) => { + if (task.title !== title) { + !updateLoading && updateTaskTitleHandler(task, title); + } + }, + [updateLoading, updateTaskTitleHandler] + ); + + /** + * Signle parent about updating and close event (that can trigger close component e.g) + */ + useEffect(() => { + if (props.loadingRef?.current && !updateLoading) { + closeable_fcRef.current && closeable_fcRef.current(); + } + + if (props.loadingRef) { + props.loadingRef.current = updateLoading; + } + }, [updateLoading, props.loadingRef, closeable_fcRef]); + + /* Setting the filter to open when the edit mode is true. */ + useEffect(() => { + editMode && setFilter('open'); + }, [editMode, setFilter]); + + /* + If task is passed then we don't want to set the active task for the authenticated user. + after task creation + */ + const autoActiveTask = props.task !== undefined ? false : true; + const handleTaskCreation = useCallback(async () => { + /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */ + datas && + datas.handleTaskCreation && + datas.hasCreateForm && + datas + .handleTaskCreation({ + autoActiveTask, + autoAssignTaskAuth: props.autoAssignTaskAuth, + assignToUsers: props.usersTaskCreatedAssignTo || [] + }) + ?.then(onTaskCreated) + .finally(async () => { + setTaskName(''); + }); + }, [datas, autoActiveTask, props.autoAssignTaskAuth, props.usersTaskCreatedAssignTo, onTaskCreated]); + + let updatedTaskList: ITeamTask[] = []; + if (props.forParentChildRelationship) { + if ( + // Story can have ParentId set to Epic ID + props.task?.issueType === 'Story' + ) { + updatedTaskList = datas.filteredTasks.filter((item) => item.issueType === 'Epic'); + } else if ( + // TASK|BUG can have ParentId to be set either to Story ID or Epic ID + props.task?.issueType === 'Task' || + props.task?.issueType === 'Bug' || + !props.task?.issueType + ) { + updatedTaskList = datas.filteredTasks.filter( + (item) => item.issueType === 'Epic' || item.issueType === 'Story' + ); + } else { + updatedTaskList = datas.filteredTasks; + } + + if (props.task?.children && props.task?.children?.length) { + const childrenTaskIds = props.task?.children?.map((item) => item.id); + updatedTaskList = updatedTaskList.filter((item) => !childrenTaskIds.includes(item.id)); + } + } + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (inputRef.current && !inputRef.current.contains(event.target as Node) && editMode) { + inputTask && updateTaskNameHandler(inputTask, taskName); + // console.log('func active'); + } + }; + + // Attach the event listener + document.addEventListener('mousedown', handleClickOutside); + + // Clean up the event listener on component unmount + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [inputTask, taskName, updateTaskNameHandler, editMode]); + + // Handling Hotkeys + const handleCommandKeySequence = useCallback(() => { + if (!editMode) { + setEditMode(true); + if (targetEl.current) { + targetEl.current.focus(); + } + } else { + setEditMode(false); + } + }, [setEditMode, editMode, targetEl]); + useHotkeys(HostKeys.CREATE_TASK, handleCommandKeySequence); + + useEffect(() => { + if (props.autoFocus && targetEl.current) { + targetEl.current.focus(); + } + }, [props.autoFocus, targetEl]); + + const inputField = ( + { + setEditMode(true); + props.autoInputSelectText && setTimeout(() => e?.target?.select(), 10); + }} + onChange={(event) => { + setTaskName(event.target.value); + }} + onKeyUp={(e) => { + if (e.key === 'Enter' && inputTask) { + /* If createOnEnterClick is false then updateTaskNameHandler is called. */ + !props.createOnEnterClick && updateTaskNameHandler(inputTask, taskName); + + props.onEnterKey && props.onEnterKey(taskName, inputTask); + } + /* Creating a new task when the enter key is pressed. */ + if (e.key === 'Enter') { + props.createOnEnterClick && handleTaskCreation(); + } + }} + trailingNode={ + /* Showing the spinner when the task is being updated. */ +
+ {props.task ? ( + (updateLoading || props.inputLoader) && + ) : ( + <>{updateLoading && } + )} +
+ } + className={clsxm( + showTaskNumber && inputTask && ['pl-2'], + 'dark:bg-[#1B1D22]', + props.initEditMode && 'h-10' + )} + /* Showing the task number. */ + leadingNode={ + // showTaskNumber && + // inputTask && +
+ { + taskIssue.current = v; + }} + /> +
+ } + /> + ); + + const taskCard = ( + + ); + + return taskCard; +} + +/** + * A component that is used to render the task list. + */ +function TaskCard({ + datas, + onItemClick, + inputField, + kanbanTitle, + handleTaskCreation, + forParentChildRelationship +}: { + datas: Partial; + onItemClick?: (task: ITeamTask) => void; + inputField?: JSX.Element; + kanbanTitle: string; + fullWidth?: boolean; + fullHeight?: boolean; + handleTaskCreation: () => void; + cardWithoutShadow?: boolean; + forParentChildRelationship?: boolean; + updatedTaskList?: ITeamTask[]; +}) { + const t = useTranslations(); + const activeTaskEl = useRef(null); + + const { taskStatus, taskPriority, taskSize, taskDescription } = datas; + useEffect(() => { + if (taskStatus) { + taskStatus.current = kanbanTitle ?? 'open'; + } + }, [taskStatus, datas.hasCreateForm, kanbanTitle]); + useEffect(() => { + if (datas.editMode) { + window.setTimeout(() => { + activeTaskEl?.current?.scrollIntoView({ + block: 'nearest', + inline: 'start' + }); + }, 10); + } + }, [datas.editMode]); + const taskStatusHook = useTaskStatus(); + + return ( + <> + + {inputField} +
+ {/* Create team button */} + +
+ {datas.hasCreateForm && ( +
+ { + if (taskDescription) { + taskDescription.current = e.target.value; + } + }} + className={'dark:bg-[#1B1D22]'} + /> + +
+ { + if (v && taskPriority) { + taskPriority.current = v; + } + }} + defaultValue={taskPriority?.current as ITaskPriority} + task={null} + /> + + { + if (v && taskSize) { + taskSize.current = v; + } + }} + defaultValue={taskSize?.current as ITaskSize} + task={null} + /> +
+
+ )} + + + + +
+
+ + + {!forParentChildRelationship && + datas.filteredTasks?.map((task, i) => { + const last = (datas.filteredTasks?.length || 0) - 1 === i; + const active = datas.inputTask === task; + + return ( +
  • + + + {!last && } +
  • + ); + })} +
    + + {/* Just some spaces at the end */} +
    {'|'}
    + + ); +}