This commit is contained in:
@@ -1024,6 +1024,7 @@
|
||||
"new_label": "برچسب جدید",
|
||||
"label_title": "عنوان",
|
||||
"choose_custom_color": "انتخاب رنگ دلخواه",
|
||||
"select_cover_color": "انتخاب رنگ کاور",
|
||||
"members": "اعضا",
|
||||
"copy_from": "کپی از",
|
||||
"none": "هیچ کدام",
|
||||
|
||||
@@ -36,7 +36,7 @@ const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick })
|
||||
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">
|
||||
{task.tag}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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 TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
||||
import { clx } from "../../../../helpers/utils";
|
||||
import type { Column } from "../../types";
|
||||
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
||||
|
||||
type Props = {
|
||||
columns: Column[];
|
||||
selectedPhaseId: string;
|
||||
onPhaseChange: (phaseId: string) => void;
|
||||
isChangingPhase?: boolean;
|
||||
taskColor: string | null;
|
||||
onColorSelect: (color: string) => void;
|
||||
isUpdatingColor?: boolean;
|
||||
onClose: () => void;
|
||||
onDelete?: () => void;
|
||||
isDeleting?: boolean;
|
||||
@@ -20,6 +24,9 @@ const TaskDetailHeader: FC<Props> = ({
|
||||
selectedPhaseId,
|
||||
onPhaseChange,
|
||||
isChangingPhase,
|
||||
taskColor,
|
||||
onColorSelect,
|
||||
isUpdatingColor,
|
||||
onClose,
|
||||
onDelete,
|
||||
isDeleting,
|
||||
@@ -65,6 +72,33 @@ const TaskDetailHeader: FC<Props> = ({
|
||||
</Popover>
|
||||
|
||||
<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 ? (
|
||||
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
||||
<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 AttachmentList from "./attachment/List";
|
||||
import CheckLists from "./checklist/List";
|
||||
import TaskDetailCoverColorBar from "./cover/TaskDetailCoverColorBar";
|
||||
import TaskDetailDescription from "./TaskDetailDescription";
|
||||
import TaskDetailHeader from "./TaskDetailHeader";
|
||||
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 [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState<string | null>(null);
|
||||
const initialTitleRef = useRef("");
|
||||
const initialDescriptionRef = useRef("");
|
||||
|
||||
@@ -50,6 +52,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
||||
setActiveTab(null);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setColor(null);
|
||||
initialTitleRef.current = "";
|
||||
initialDescriptionRef.current = "";
|
||||
}
|
||||
@@ -60,6 +63,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
||||
|
||||
setTitle(taskDetail.title);
|
||||
setDescription(taskDetail.description ?? "");
|
||||
setColor(taskDetail.color || null);
|
||||
initialTitleRef.current = taskDetail.title;
|
||||
initialDescriptionRef.current = taskDetail.description ?? "";
|
||||
}, [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) => {
|
||||
if (taskPhaseId === selectedPhaseId) return;
|
||||
|
||||
@@ -144,11 +166,22 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
||||
selectedPhaseId={selectedPhaseId}
|
||||
onPhaseChange={handlePhaseChange}
|
||||
isChangingPhase={changeTaskPhase.isPending}
|
||||
taskColor={color}
|
||||
onColorSelect={(newColor) => handleColorChange(newColor)}
|
||||
isUpdatingColor={updateTask.isPending}
|
||||
onClose={onClose}
|
||||
onDelete={handleDelete}
|
||||
isDeleting={deleteTask.isPending}
|
||||
/>
|
||||
|
||||
{color ? (
|
||||
<TaskDetailCoverColorBar
|
||||
color={color}
|
||||
onRemove={() => handleColorChange(null)}
|
||||
isRemoving={updateTask.isPending}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
value={title}
|
||||
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;
|
||||
tag?: string;
|
||||
dateRange?: string;
|
||||
color?: string | null;
|
||||
attachments?: number;
|
||||
checklistDone?: number;
|
||||
checklistTotal?: number;
|
||||
@@ -114,7 +115,7 @@ export type ChangeTaskPhaseType = {
|
||||
export type UpdateTaskType = Partial<
|
||||
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
||||
> & {
|
||||
color?: string;
|
||||
color?: string | null;
|
||||
userIds?: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallb
|
||||
title: task.title,
|
||||
tag: task.tag ?? "",
|
||||
dateRange: task.dateRange ?? "",
|
||||
color: task.color || null,
|
||||
attachments: task.attachments ?? 0,
|
||||
checklistDone: task.checklistDone ?? 0,
|
||||
checklistTotal: task.checklistTotal ?? 0,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Task = {
|
||||
title: string;
|
||||
tag: string;
|
||||
dateRange: string;
|
||||
color?: string | null;
|
||||
attachments: number;
|
||||
checklistDone: number;
|
||||
checklistTotal: number;
|
||||
|
||||
Reference in New Issue
Block a user