color task
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-07-14 11:28:10 +03:30
parent a356a3fcbd
commit 1963a351d0
10 changed files with 148 additions and 3 deletions
+1
View File
@@ -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": "هیچ کدام",
+1 -1
View File
@@ -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,
+1
View File
@@ -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;