CREATE AND DELETE column
This commit is contained in:
@@ -1000,6 +1000,12 @@
|
||||
"select_date": "انتخاب تاریخ",
|
||||
"projects": "پروژهها",
|
||||
"delete_project": "پاک کردن",
|
||||
"add_new_column": "اضافه کردن ستون جدید",
|
||||
"add_column": "اضافه کردن",
|
||||
"column_name": "نام ستون",
|
||||
"delete_column": "حذف ستون",
|
||||
"delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟",
|
||||
"column_deleted": "ستون با موفقیت حذف شد",
|
||||
"task_detail": {
|
||||
"labels": "برچسب ها",
|
||||
"user_management": "مدیریت کاربران",
|
||||
|
||||
@@ -1,12 +1,146 @@
|
||||
import { AddCircle } from "iconsax-react";
|
||||
import { type FC } from "react";
|
||||
import { Add, AddCircle, CloseCircle } from "iconsax-react";
|
||||
import { useRef, useState, type FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "react-toastify";
|
||||
import ColorsImage from "../../../assets/images/colors.png";
|
||||
import { ErrorType } from "../../../helpers/types";
|
||||
import { clx } from "../../../helpers/utils";
|
||||
import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||||
import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes";
|
||||
import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants";
|
||||
|
||||
const DEFAULT_COLUMN_COLOR = "#4F46E5";
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
nextOrder: number;
|
||||
onColumnCreated: (phase: TaskPhaseItemType) => void;
|
||||
};
|
||||
|
||||
const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
||||
const { t } = useTranslation("global");
|
||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||
const createTaskPhase = useCreateTaskPhase();
|
||||
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [selectedColor, setSelectedColor] = useState(DEFAULT_COLUMN_COLOR);
|
||||
|
||||
const resetForm = () => {
|
||||
setName("");
|
||||
setSelectedColor(DEFAULT_COLUMN_COLOR);
|
||||
setIsAdding(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const trimmedName = name.trim();
|
||||
if (!trimmedName || !projectId) return;
|
||||
|
||||
createTaskPhase.mutate(
|
||||
{
|
||||
name: trimmedName,
|
||||
order: nextOrder,
|
||||
color: selectedColor,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const phase = (data?.data ?? data) as TaskPhaseItemType;
|
||||
onColumnCreated(phase);
|
||||
toast.success(t("success"));
|
||||
resetForm();
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0]);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (!isAdding) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAdding(true)}
|
||||
className="bg-[#F0F3F7BF] bg-opacity-75 rounded-full px-4 sm:px-6 py-3 sm:py-4 flex items-center gap-2 shrink-0 self-start cursor-pointer w-[272px] sm:w-[288px] xl:w-[310px] justify-center"
|
||||
>
|
||||
<AddCircle size={16} color="black" />
|
||||
<span className="text-xs text-black">{t("taskmanager.add_new_column")}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const AddNewColumn: FC = () => {
|
||||
return (
|
||||
<button type="button" className="bg-[#F0F3F7BF] bg-opacity-75 rounded-full px-4 sm:px-6 py-3 sm:py-4 flex items-center gap-2 shrink-0 self-start cursor-pointer w-[272px] sm:w-[288px] xl:w-[310px] justify-center">
|
||||
<AddCircle size={16} color="black" />
|
||||
<span className="text-xs text-black">اضافه کردن ستون جدید</span>
|
||||
</button>
|
||||
<div className="bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 self-start flex flex-col gap-3 sm:gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("taskmanager.column_name")}
|
||||
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="text-xs mb-3">{t("taskmanager.choose_color")}</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => colorInputRef.current?.click()}
|
||||
className={clx(
|
||||
"size-6 rounded-lg cursor-pointer overflow-hidden",
|
||||
!LABEL_COLOR_OPTIONS.includes(
|
||||
selectedColor as (typeof LABEL_COLOR_OPTIONS)[number],
|
||||
) && "ring-1 ring-[#0047FF] rounded-full",
|
||||
)}
|
||||
aria-label={t("taskmanager.choose_color")}
|
||||
>
|
||||
<img src={ColorsImage} alt="" className="size-full object-cover" />
|
||||
</button>
|
||||
{LABEL_COLOR_OPTIONS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => setSelectedColor(color)}
|
||||
className={clx(
|
||||
"size-6 rounded-lg cursor-pointer transition-transform",
|
||||
color === "#FFFFFF" && "border border-[#D0D0D0]",
|
||||
selectedColor === color && "ring-2 ring-[#0047FF] ring-offset-1",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
aria-label={color}
|
||||
/>
|
||||
))}
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
value={selectedColor}
|
||||
onChange={(e) => setSelectedColor(e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!name.trim() || createTaskPhase.isPending}
|
||||
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<span>{t("taskmanager.add_column")}</span>
|
||||
<Add size={18} color="white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
className="cursor-pointer"
|
||||
aria-label={t("cancel")}
|
||||
>
|
||||
<CloseCircle size={22} color="#888888" variant="Bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Add, AddCircle, CloseCircle, More } from "iconsax-react";
|
||||
import { Add, AddCircle, CloseCircle } from "iconsax-react";
|
||||
import { useRef, useState, type FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "react-toastify";
|
||||
import ModalConfrim from "../../../components/ModalConfrim";
|
||||
import { ErrorType } from "../../../helpers/types";
|
||||
import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||||
import type { Column as ColumnType, Task as TaskType } from "../types";
|
||||
import ColumnMenu from "./ColumnMenu";
|
||||
import Task from "./Task";
|
||||
|
||||
type Props = {
|
||||
@@ -15,6 +21,7 @@ type Props = {
|
||||
onColumnDragEnd: () => void;
|
||||
onColumnDrop: (overColumnId: string) => void;
|
||||
onTaskClick?: (task: TaskType) => void;
|
||||
onColumnDeleted?: (columnId: string) => void;
|
||||
};
|
||||
|
||||
const Column: FC<Props> = ({
|
||||
@@ -29,8 +36,13 @@ const Column: FC<Props> = ({
|
||||
onColumnDragEnd,
|
||||
onColumnDrop,
|
||||
onTaskClick,
|
||||
onColumnDeleted,
|
||||
}) => {
|
||||
const { t } = useTranslation("global");
|
||||
const deleteTaskPhase = useDeleteTaskPhase();
|
||||
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
||||
const columnRef = useRef<HTMLDivElement>(null);
|
||||
@@ -116,6 +128,19 @@ const Column: FC<Props> = ({
|
||||
onColumnDrop(column.id);
|
||||
};
|
||||
|
||||
const handleDeleteColumn = () => {
|
||||
deleteTaskPhase.mutate(column.id, {
|
||||
onSuccess: () => {
|
||||
onColumnDeleted?.(column.id);
|
||||
setIsDeleteConfirmOpen(false);
|
||||
toast.success(t("taskmanager.column_deleted"));
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={columnRef}
|
||||
@@ -139,16 +164,26 @@ const Column: FC<Props> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={handleColumnDragStart}
|
||||
onDragEnd={handleColumnDragEnd}
|
||||
className="flex justify-between items-center shrink-0 cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<div className="font-bold text-sm truncate">{column.title}</div>
|
||||
<More size={20} color="black" className="shrink-0 pointer-events-none" />
|
||||
<div className="flex justify-between items-center shrink-0 gap-2">
|
||||
<div
|
||||
draggable
|
||||
onDragStart={handleColumnDragStart}
|
||||
onDragEnd={handleColumnDragEnd}
|
||||
className="flex-1 min-w-0 font-bold text-sm truncate cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
{column.title}
|
||||
</div>
|
||||
<ColumnMenu onDelete={() => setIsDeleteConfirmOpen(true)} />
|
||||
</div>
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={isDeleteConfirmOpen}
|
||||
close={() => setIsDeleteConfirmOpen(false)}
|
||||
onConfrim={handleDeleteColumn}
|
||||
isLoading={deleteTaskPhase.isPending}
|
||||
label={t("taskmanager.delete_column_confirm")}
|
||||
/>
|
||||
|
||||
<div className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4">
|
||||
{tasks.map((task, index) => (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { More } from "iconsax-react";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const ColumnMenu: FC<Props> = ({ onDelete }) => {
|
||||
const { t } = useTranslation("global");
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<PopoverButton
|
||||
className="flex items-center justify-center size-8 rounded-lg hover:bg-black/5 transition-colors outline-none shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<More size={20} color="black" />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel
|
||||
anchor="bottom start"
|
||||
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
className="w-full px-4 py-2.5 text-right text-red-500 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
{t("taskmanager.delete_column")}
|
||||
</button>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnMenu;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "../service/TaskPhaseService";
|
||||
import { CreateTaskPhaseType } from "../types/TaskPhaseTypes";
|
||||
|
||||
export const useCreateTaskPhase = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateTaskPhaseType) => api.createTaskPhase(variables),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["task-phases", variables.projectId],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteTaskPhase = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteTaskPhase(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["task-phases"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import axios from "../../../../config/axios";
|
||||
import { CreateTaskPhaseType } from "../types/TaskPhaseTypes";
|
||||
|
||||
export const createTaskPhase = async (params: CreateTaskPhaseType) => {
|
||||
const { data } = await axios.post(`/task-manager/task-phases`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteTaskPhase = async (id: string) => {
|
||||
const { data } = await axios.delete(`/task-manager/task-phases/${id}`);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export type CreateTaskPhaseType = {
|
||||
name: string;
|
||||
order: number;
|
||||
color: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TaskPhaseItemType = {
|
||||
id: string;
|
||||
name: string;
|
||||
order: number;
|
||||
color: string;
|
||||
projectId: string;
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
export type Column = {
|
||||
id: string;
|
||||
title: string;
|
||||
color?: string;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
export type Task = {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useMemo, useState, type FC } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import AddNewColumn from "./components/AddNewColumn";
|
||||
import Column from "./components/Column";
|
||||
import HeaderWorkspace from "./components/HeaderWorkspace";
|
||||
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
||||
import tasksData from "./data/tasks.json";
|
||||
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
||||
import type { Task, WorkspaceData } from "./types";
|
||||
import { reorderColumns } from "./utils/reorderColumns";
|
||||
import { reorderTasks } from "./utils/reorderTasks";
|
||||
|
||||
const Workspace: FC = () => {
|
||||
const { slug: projectId = "" } = useParams<{ slug: string }>();
|
||||
const [columns, setColumns] = useState(
|
||||
(tasksData as WorkspaceData).columns,
|
||||
);
|
||||
@@ -54,6 +57,26 @@ const Workspace: FC = () => {
|
||||
setDraggedColumnId(null);
|
||||
};
|
||||
|
||||
const handleColumnCreated = (phase: TaskPhaseItemType) => {
|
||||
setColumns((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: phase.id,
|
||||
title: phase.name,
|
||||
color: phase.color,
|
||||
order: phase.order,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleColumnDeleted = (columnId: string) => {
|
||||
setColumns((prev) => prev.filter((column) => column.id !== columnId));
|
||||
setTasks((prev) => prev.filter((task) => task.columnId !== columnId));
|
||||
if (selectedTask?.columnId === columnId) {
|
||||
setSelectedTask(null);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedTaskColumn = selectedTask
|
||||
? columns.find((column) => column.id === selectedTask.columnId)
|
||||
: null;
|
||||
@@ -77,10 +100,15 @@ const Workspace: FC = () => {
|
||||
onColumnDragEnd={handleColumnDragEnd}
|
||||
onColumnDrop={handleColumnDrop}
|
||||
onTaskClick={setSelectedTask}
|
||||
onColumnDeleted={handleColumnDeleted}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddNewColumn />
|
||||
<AddNewColumn
|
||||
projectId={projectId}
|
||||
nextOrder={columns.length}
|
||||
onColumnCreated={handleColumnCreated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user