This commit is contained in:
@@ -1024,6 +1024,7 @@
|
|||||||
"new_label": "برچسب جدید",
|
"new_label": "برچسب جدید",
|
||||||
"label_title": "عنوان",
|
"label_title": "عنوان",
|
||||||
"choose_custom_color": "انتخاب رنگ دلخواه",
|
"choose_custom_color": "انتخاب رنگ دلخواه",
|
||||||
|
"select_cover_color": "انتخاب رنگ کاور",
|
||||||
"members": "اعضا",
|
"members": "اعضا",
|
||||||
"copy_from": "کپی از",
|
"copy_from": "کپی از",
|
||||||
"none": "هیچ کدام",
|
"none": "هیچ کدام",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick })
|
|||||||
isDragging ? "opacity-40" : ""
|
isDragging ? "opacity-40" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-10 bg-yellow-500 rounded-lg"></div>
|
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
||||||
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
|
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
|
||||||
{task.tag}
|
{task.tag}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import { ArrowDown2, CloseCircle } from "iconsax-react";
|
import { ArrowDown2, CloseCircle, Paintbucket } from "iconsax-react";
|
||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
||||||
import { clx } from "../../../../helpers/utils";
|
import { clx } from "../../../../helpers/utils";
|
||||||
import type { Column } from "../../types";
|
import type { Column } from "../../types";
|
||||||
|
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
selectedPhaseId: string;
|
selectedPhaseId: string;
|
||||||
onPhaseChange: (phaseId: string) => void;
|
onPhaseChange: (phaseId: string) => void;
|
||||||
isChangingPhase?: boolean;
|
isChangingPhase?: boolean;
|
||||||
|
taskColor: string | null;
|
||||||
|
onColorSelect: (color: string) => void;
|
||||||
|
isUpdatingColor?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
@@ -20,6 +24,9 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
selectedPhaseId,
|
selectedPhaseId,
|
||||||
onPhaseChange,
|
onPhaseChange,
|
||||||
isChangingPhase,
|
isChangingPhase,
|
||||||
|
taskColor,
|
||||||
|
onColorSelect,
|
||||||
|
isUpdatingColor,
|
||||||
onClose,
|
onClose,
|
||||||
onDelete,
|
onDelete,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
@@ -65,6 +72,33 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Popover className="relative">
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<PopoverButton
|
||||||
|
disabled={isUpdatingColor}
|
||||||
|
className="size-8 rounded-full bg-white/60 flex items-center justify-center cursor-pointer disabled:opacity-50 outline-none"
|
||||||
|
aria-label="انتخاب رنگ کاور"
|
||||||
|
>
|
||||||
|
<Paintbucket size={18} color="#292D32" />
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="bottom end"
|
||||||
|
className="z-[80] mt-1 rounded-2xl bg-white shadow-md border border-border p-4"
|
||||||
|
>
|
||||||
|
<TaskDetailCoverColorPopover
|
||||||
|
selectedColor={taskColor}
|
||||||
|
onSelect={(color) => {
|
||||||
|
onColorSelect(color);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverPanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
{onDelete ? (
|
{onDelete ? (
|
||||||
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
||||||
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { TaskDetailType } from "../../task/types/TaskTypes";
|
|||||||
import type { Column, Task } from "../../types";
|
import type { Column, Task } from "../../types";
|
||||||
import AttachmentList from "./attachment/List";
|
import AttachmentList from "./attachment/List";
|
||||||
import CheckLists from "./checklist/List";
|
import CheckLists from "./checklist/List";
|
||||||
|
import TaskDetailCoverColorBar from "./cover/TaskDetailCoverColorBar";
|
||||||
import TaskDetailDescription from "./TaskDetailDescription";
|
import TaskDetailDescription from "./TaskDetailDescription";
|
||||||
import TaskDetailHeader from "./TaskDetailHeader";
|
import TaskDetailHeader from "./TaskDetailHeader";
|
||||||
import TaskDetailToolbar from "./TaskDetailToolbar";
|
import TaskDetailToolbar from "./TaskDetailToolbar";
|
||||||
@@ -28,6 +29,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
|
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [color, setColor] = useState<string | null>(null);
|
||||||
const initialTitleRef = useRef("");
|
const initialTitleRef = useRef("");
|
||||||
const initialDescriptionRef = useRef("");
|
const initialDescriptionRef = useRef("");
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
setActiveTab(null);
|
setActiveTab(null);
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setColor(null);
|
||||||
initialTitleRef.current = "";
|
initialTitleRef.current = "";
|
||||||
initialDescriptionRef.current = "";
|
initialDescriptionRef.current = "";
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
|
|
||||||
setTitle(taskDetail.title);
|
setTitle(taskDetail.title);
|
||||||
setDescription(taskDetail.description ?? "");
|
setDescription(taskDetail.description ?? "");
|
||||||
|
setColor(taskDetail.color || null);
|
||||||
initialTitleRef.current = taskDetail.title;
|
initialTitleRef.current = taskDetail.title;
|
||||||
initialDescriptionRef.current = taskDetail.description ?? "";
|
initialDescriptionRef.current = taskDetail.description ?? "";
|
||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
@@ -116,6 +120,24 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleColorChange = (newColor: string | null) => {
|
||||||
|
const previousColor = color;
|
||||||
|
setColor(newColor);
|
||||||
|
|
||||||
|
updateTask.mutate(
|
||||||
|
{ id: task.id, params: { color: newColor ?? "" } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("success"));
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
setColor(previousColor);
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePhaseChange = (taskPhaseId: string) => {
|
const handlePhaseChange = (taskPhaseId: string) => {
|
||||||
if (taskPhaseId === selectedPhaseId) return;
|
if (taskPhaseId === selectedPhaseId) return;
|
||||||
|
|
||||||
@@ -144,11 +166,22 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
selectedPhaseId={selectedPhaseId}
|
selectedPhaseId={selectedPhaseId}
|
||||||
onPhaseChange={handlePhaseChange}
|
onPhaseChange={handlePhaseChange}
|
||||||
isChangingPhase={changeTaskPhase.isPending}
|
isChangingPhase={changeTaskPhase.isPending}
|
||||||
|
taskColor={color}
|
||||||
|
onColorSelect={(newColor) => handleColorChange(newColor)}
|
||||||
|
isUpdatingColor={updateTask.isPending}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
isDeleting={deleteTask.isPending}
|
isDeleting={deleteTask.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{color ? (
|
||||||
|
<TaskDetailCoverColorBar
|
||||||
|
color={color}
|
||||||
|
onRemove={() => handleColorChange(null)}
|
||||||
|
isRemoving={updateTask.isPending}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Trash } from "iconsax-react";
|
||||||
|
import { type FC } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
color: string;
|
||||||
|
onRemove: () => void;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetailCoverColorBar: FC<Props> = ({ color, onRemove, isRemoving }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative mt-4 -mx-1">
|
||||||
|
<div className="h-10 rounded-xl" style={{ backgroundColor: color }} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
disabled={isRemoving}
|
||||||
|
className="absolute bottom-1 left-2 size-7 rounded-full bg-white/80 flex items-center justify-center cursor-pointer disabled:opacity-50"
|
||||||
|
aria-label="حذف رنگ"
|
||||||
|
>
|
||||||
|
<Trash size={16} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetailCoverColorBar;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { type FC } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { clx } from "../../../../../helpers/utils";
|
||||||
|
import { TASK_COVER_COLORS } from "./constants";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedColor: string | null;
|
||||||
|
onSelect: (color: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetailCoverColorPopover: FC<Props> = ({ selectedColor, onSelect }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[280px]">
|
||||||
|
<div className="text-xs font-medium mb-3 text-right">{t("taskmanager.task_detail.select_cover_color")}</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{TASK_COVER_COLORS.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(color)}
|
||||||
|
className={clx(
|
||||||
|
"h-10 rounded-lg cursor-pointer transition-shadow",
|
||||||
|
color === "#D1D5DB" && "border border-[#B0B0B0]",
|
||||||
|
selectedColor === color && "ring-2 ring-black ring-offset-1",
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
aria-label={color}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetailCoverColorPopover;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const TASK_COVER_COLORS = [
|
||||||
|
"#FF6B9D",
|
||||||
|
"#FF9F43",
|
||||||
|
"#6EE7B7",
|
||||||
|
"#60A5FA",
|
||||||
|
"#A78BFA",
|
||||||
|
"#EF4444",
|
||||||
|
"#D1D5DB",
|
||||||
|
"#FDE047",
|
||||||
|
] as const;
|
||||||
@@ -14,6 +14,7 @@ export type TaskItemType = {
|
|||||||
order: number;
|
order: number;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
dateRange?: string;
|
dateRange?: string;
|
||||||
|
color?: string | null;
|
||||||
attachments?: number;
|
attachments?: number;
|
||||||
checklistDone?: number;
|
checklistDone?: number;
|
||||||
checklistTotal?: number;
|
checklistTotal?: number;
|
||||||
@@ -114,7 +115,7 @@ export type ChangeTaskPhaseType = {
|
|||||||
export type UpdateTaskType = Partial<
|
export type UpdateTaskType = Partial<
|
||||||
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
||||||
> & {
|
> & {
|
||||||
color?: string;
|
color?: string | null;
|
||||||
userIds?: string[];
|
userIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallb
|
|||||||
title: task.title,
|
title: task.title,
|
||||||
tag: task.tag ?? "",
|
tag: task.tag ?? "",
|
||||||
dateRange: task.dateRange ?? "",
|
dateRange: task.dateRange ?? "",
|
||||||
|
color: task.color || null,
|
||||||
attachments: task.attachments ?? 0,
|
attachments: task.attachments ?? 0,
|
||||||
checklistDone: task.checklistDone ?? 0,
|
checklistDone: task.checklistDone ?? 0,
|
||||||
checklistTotal: task.checklistTotal ?? 0,
|
checklistTotal: task.checklistTotal ?? 0,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type Task = {
|
|||||||
title: string;
|
title: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
dateRange: string;
|
dateRange: string;
|
||||||
|
color?: string | null;
|
||||||
attachments: number;
|
attachments: number;
|
||||||
checklistDone: number;
|
checklistDone: number;
|
||||||
checklistTotal: number;
|
checklistTotal: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user