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": "برچسب جدید",
"label_title": "عنوان",
"choose_custom_color": "انتخاب رنگ دلخواه",
"select_cover_color": "انتخاب رنگ کاور",
"members": "اعضا",
"copy_from": "کپی از",
"none": "هیچ کدام",
+1 -1
View File
@@ -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,
+1
View File
@@ -12,6 +12,7 @@ export type Task = {
title: string;
tag: string;
dateRange: string;
color?: string | null;
attachments: number;
checklistDone: number;
checklistTotal: number;