diff --git a/src/components/ui/Task/status.tsx b/src/components/ui/Task/status.tsx index 1631429..be751f9 100644 --- a/src/components/ui/Task/status.tsx +++ b/src/components/ui/Task/status.tsx @@ -286,3 +286,6 @@ export const StatusSelectorByTask = ({ task, type, iconVariant }: StatusSelector ); }; + + + diff --git a/src/modules/tasks/index.tsx b/src/modules/tasks/index.tsx index 6bd05b7..8909080 100644 --- a/src/modules/tasks/index.tsx +++ b/src/modules/tasks/index.tsx @@ -14,9 +14,8 @@ import { ActionIcon, useMantineTheme, } from "@mantine/core"; -import { useListState } from "@mantine/hooks"; import { useEffect, useState } from "react"; -import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; +import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { LayoutColumns, LayoutRows, LayoutSidebar } from "tabler-icons-react"; import { getCookie, setCookie } from "cookies-next"; import { useQuery } from "urql"; @@ -27,6 +26,8 @@ import { StatusIcon, statusName } from "components/ui/Task/status"; import { usePlexoContext } from "context/PlexoContext"; import { Task } from "lib/types"; import FilterMenu from "components/ui/Filters/filterMenu"; +import { useActions } from "lib/hooks/useActions"; +import { ErrorNotification, SuccessNotification } from "lib/notifications"; const useStyles = createStyles(theme => ({ burger: { @@ -68,69 +69,63 @@ const useStyles = createStyles(theme => ({ }, })); -const DndTaskBoard = ({ statusData }: { statusData: Task[] }) => { - const [state, handlers] = useListState([...statusData]); +type TaskBoardProps = { + statusData: Task[]; + currentStatus: TaskStatus; +}; +const DndTaskBoard = ({ statusData, currentStatus }: TaskBoardProps) => { return ( - { - handlers.reorder({ from: source.index, to: destination?.index || 0 }); - }} - > - - {provided => ( - - {state.map((t: Task, index: number) => ( - - {provided => ( - - - - )} - - ))} - {provided.placeholder} - - )} - - + + {provided => ( + + {statusData.map((task: Task, index: number) => ( + + {provided => ( + + + + )} + + ))} + {provided.placeholder} + + )} + ); }; -const DndTaskList = ({ statusData }: { statusData: Task[] }) => { - const [state, handlers] = useListState([...statusData]); +type TaskListProps = { + statusData: Task[]; + currentStatus: TaskStatus; +}; +const DndTaskList = ({ statusData, currentStatus }: TaskListProps) => { return ( - { - handlers.reorder({ from: source.index, to: destination?.index || 0 }); - }} - > - - {provided => ( - - {state.map((t: Task, index: number) => ( - - {provided => ( - - - - )} - - ))} - {provided.placeholder} - - )} - - + + {provided => ( + + {statusData.map((task: Task, index: number) => ( + + {provided => ( + + + + )} + + ))} + {provided.placeholder} + + )} + ); }; @@ -139,10 +134,6 @@ type StatusCounterProps = { taskData: Task[] | undefined; }; -type TaskProps = { - status: TaskStatus; -}; - type TasksProps = { taskData: Task[] | undefined; fetching: boolean; @@ -177,10 +168,6 @@ const TasksBoard = ({ taskData, fetching }: TasksProps) => { return data; }; - const TaskCard = ({ status }: TaskProps) => { - return ; - }; - const Counter = ({ status, fetching }: CounterProps) => { return dataByStatus(status).length || fetching ? ( @@ -206,86 +193,148 @@ const TasksBoard = ({ taskData, fetching }: TasksProps) => { return colsCounter > 6 ? 6 : colsCounter; }; + const { fetchUpdateTask } = useActions(); + const ChangeTaskStatus = async (status: TaskStatus, taskId: String, fetchUpdateTask: any) => { + const res = await fetchUpdateTask({ + taskId: taskId, + status: statusName(status), + }); + + if (res.data) { + SuccessNotification("Status updated", res.data.updateTask.title); + return true; + + //move visually droppable element from one column to another + } + if (res.error) { + ErrorNotification(); + return false; + } + }; + + const initialStatusMap: StatusMap = { + [TaskStatus.None]: dataByStatus(TaskStatus.None), + [TaskStatus.Backlog]: dataByStatus(TaskStatus.Backlog), + [TaskStatus.ToDo]: dataByStatus(TaskStatus.ToDo), + [TaskStatus.InProgress]: dataByStatus(TaskStatus.InProgress), + [TaskStatus.Done]: dataByStatus(TaskStatus.Done), + [TaskStatus.Canceled]: dataByStatus(TaskStatus.Canceled), + }; + + useEffect(() => { + setColumns(initialStatusMap); + }, [taskData]); + + type StatusMap = { + [status in TaskStatus]: Task[]; + }; + + const [columns, setColumns] = useState(initialStatusMap); + const [ordered, setOrdered] = useState( + Object.keys(initialStatusMap) as TaskStatus[] + ); + const [lastUpdated, setLastUpdated] = useState(null); + + const onDragEnd = async (result: DropResult) => { + const { destination, source, draggableId } = result; + + // If there's no destination (e.g., item was dropped outside the list), do nothing + if (!destination) { + return; + } + + // If the item was dropped in the same place, do nothing + if (destination.droppableId === source.droppableId && destination.index === source.index) { + return; + } + + // We update the UI + + // Create a shallow copy of the current columns + const newColumns = { ...columns }; + + // Remove the task from the source column + const sourceTasks = [...newColumns[source.droppableId as TaskStatus]]; + const [movedTask] = sourceTasks.splice(source.index, 1); + newColumns[source.droppableId as TaskStatus] = sourceTasks; + + // Add the task to the destination column + const destinationTasks = [...newColumns[destination.droppableId as TaskStatus]]; + + //update the task status visual locally + movedTask.status = destination.droppableId as TaskStatus; + + // Add the task to the destination column pt2 + destinationTasks.splice(destination.index, 0, movedTask); + newColumns[destination.droppableId as TaskStatus] = destinationTasks; + + //update the task status visual locally + + // Update the state with the new columns + setColumns(newColumns); + setLastUpdated(new Date()); + + // Update the task's status in the backend + const newStatus = destination.droppableId as TaskStatus; + const wasSuccessful = await ChangeTaskStatus(newStatus, draggableId, fetchUpdateTask); + + // If the backend update was unsuccessful, update the UI to return to the previous position + if (!wasSuccessful) { + // Create a shallow copy of the modified columns + const revertedColumns = { ...newColumns }; + + // Remove the task from the destination column + const currentDestinationTasks = [...revertedColumns[destination.droppableId as TaskStatus]]; + currentDestinationTasks.splice(destination.index, 1); + revertedColumns[destination.droppableId as TaskStatus] = currentDestinationTasks; + + // Add the task back to the source column at its original position + const currentSourceTasks = [...revertedColumns[source.droppableId as TaskStatus]]; + + //update the task status visual locally + movedTask.status = source.droppableId as TaskStatus; + + // Add the task back to the source column at its original position pt2 + currentSourceTasks.splice(source.index, 0, movedTask); + revertedColumns[source.droppableId as TaskStatus] = currentSourceTasks; + + // Update the state with the reverted columns + setColumns(revertedColumns); + } else { + //we need to reload the whole column + } + setOrdered(Object.keys(newColumns) as TaskStatus[]); + }; + return ( - {StatusBoardEnable(TaskStatus.None) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} - {StatusBoardEnable(TaskStatus.Backlog) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} - {StatusBoardEnable(TaskStatus.ToDo) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} - {StatusBoardEnable(TaskStatus.InProgress) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} - {StatusBoardEnable(TaskStatus.Done) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} - {StatusBoardEnable(TaskStatus.Canceled) && ( - - - - {fetching ? ( - - ) : ( - - )} - - - )} + + {ordered.map( + (key, index) => + StatusBoardEnable(key as TaskStatus) && ( + + + + {fetching ? ( + + ) : ( + + )} + + + ) + )} + ); }; +type StatusMap = { + [status in TaskStatus]: Task[]; +}; + const TasksList = ({ taskData, fetching }: TasksProps) => { const dataByStatus = (status: TaskStatus) => { const data = taskData @@ -296,10 +345,6 @@ const TasksList = ({ taskData, fetching }: TasksProps) => { return data; }; - const TaskList = ({ status }: TaskProps) => { - return ; - }; - const Counter = ({ status, fetching }: CounterProps) => { return dataByStatus(status).length || fetching ? ( @@ -308,34 +353,129 @@ const TasksList = ({ taskData, fetching }: TasksProps) => { ); }; + const { fetchUpdateTask } = useActions(); + + const ChangeTaskStatus = async (status: TaskStatus, taskId: String, fetchUpdateTask: any) => { + const res = await fetchUpdateTask({ + taskId: taskId, + status: statusName(status), + }); + + if (res.data) { + SuccessNotification("Status updated", res.data.updateTask.title); + return true; + + //move visually droppable element from one column to another + } + if (res.error) { + ErrorNotification(); + return false; + } + }; + + const initialStatusMap: StatusMap = { + [TaskStatus.None]: dataByStatus(TaskStatus.None), + [TaskStatus.Backlog]: dataByStatus(TaskStatus.Backlog), + [TaskStatus.ToDo]: dataByStatus(TaskStatus.ToDo), + [TaskStatus.InProgress]: dataByStatus(TaskStatus.InProgress), + [TaskStatus.Done]: dataByStatus(TaskStatus.Done), + [TaskStatus.Canceled]: dataByStatus(TaskStatus.Canceled), + }; + + useEffect(() => { + setColumns(initialStatusMap); + }, [taskData]); + + const [columns, setColumns] = useState(initialStatusMap); + const [ordered, setOrdered] = useState( + Object.keys(initialStatusMap) as TaskStatus[] + ); + const [lastUpdated, setLastUpdated] = useState(null); + + const onDragEnd = async (result: DropResult) => { + const { destination, source, draggableId } = result; + + // If there's no destination (e.g., item was dropped outside the list), do nothing + if (!destination) { + return; + } + + // If the item was dropped in the same place, do nothing + if (destination.droppableId === source.droppableId && destination.index === source.index) { + return; + } + + // We update the UI + + // Create a shallow copy of the current columns + const newColumns = { ...columns }; + + // Remove the task from the source column + const sourceTasks = [...newColumns[source.droppableId as TaskStatus]]; + const [movedTask] = sourceTasks.splice(source.index, 1); + newColumns[source.droppableId as TaskStatus] = sourceTasks; + + // Add the task to the destination column + const destinationTasks = [...newColumns[destination.droppableId as TaskStatus]]; + + //update the task status visual locally + movedTask.status = destination.droppableId as TaskStatus; + + // Add the task to the destination column pt2 + destinationTasks.splice(destination.index, 0, movedTask); + newColumns[destination.droppableId as TaskStatus] = destinationTasks; + + // Update the state with the new columns + setColumns(newColumns); + setLastUpdated(new Date()); + + // Update the task's status in the backend + const newStatus = destination.droppableId as TaskStatus; + const wasSuccessful = await ChangeTaskStatus(newStatus, draggableId, fetchUpdateTask); + + // If the backend update was unsuccessful, update the UI to return to the previous position + if (!wasSuccessful) { + // Create a shallow copy of the modified columns + const revertedColumns = { ...newColumns }; + + // Remove the task from the destination column + const currentDestinationTasks = [...revertedColumns[destination.droppableId as TaskStatus]]; + currentDestinationTasks.splice(destination.index, 1); + revertedColumns[destination.droppableId as TaskStatus] = currentDestinationTasks; + + // Add the task back to the source column at its original position + const currentSourceTasks = [...revertedColumns[source.droppableId as TaskStatus]]; + + //update the task status visual locally + movedTask.status = source.droppableId as TaskStatus; + + // Add the task back to the source column at its original position pt2 + currentSourceTasks.splice(source.index, 0, movedTask); + revertedColumns[source.droppableId as TaskStatus] = currentSourceTasks; + + // Update the state with the reverted columns + setColumns(revertedColumns); + } else { + //we need to reload the whole column + } + setOrdered(Object.keys(newColumns) as TaskStatus[]); + }; + return ( - - {fetching ? : } - - - {fetching ? : } - - - {fetching ? : } - - - {fetching ? ( - - ) : ( - - )} - - - {fetching ? : } - - - {fetching ? ( - - ) : ( - - )} + + {ordered.map((key, index) => ( + + + {fetching ? ( + + ) : ( + + )} + + ))} + ); @@ -394,8 +534,8 @@ export const TasksPageContent = () => { labels: labelsFilters, project: projectFilters, }; - let filtrosTotal = Object.values(filterValues).filter(value => value.length > 0); - setTotal(filtrosTotal.length); + let totalFilters = Object.values(filterValues).filter(value => value.length > 0); + setTotal(totalFilters.length); localStorage.setItem(STORAGE_KEY, JSON.stringify(filterValues)); }, [ statusFilters,