attachment

This commit is contained in:
hamid zarghami
2026-07-12 12:13:31 +03:30
parent 7c2d6c465e
commit c11c972c7c
9 changed files with 96 additions and 25 deletions
@@ -17,7 +17,8 @@ import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
import type { TaskDetailTab } from "./types";
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
import type { TaskDetailType } from "../../task/types/TaskTypes";
import { useCreateCheckListItem, useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
import { useCreateCheckListItem, useCreateTaskAttachment, useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
import { useSingleUpload } from "../../../service/hooks/useServiceData";
import { ErrorType } from "../../../../helpers/types";
type Props = {
@@ -37,6 +38,8 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const { t } = useTranslation("global");
const createTaskRemark = useCreateTaskRemark();
const createCheckListItem = useCreateCheckListItem();
const createTaskAttachment = useCreateTaskAttachment();
const singleUpload = useSingleUpload();
const updateTask = useUpdateTask();
const getUsers = useGetUsers("");
const [labelsView, setLabelsView] = useState<LabelsView>("list");
@@ -204,10 +207,46 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
if (isAttachmentOpen) onTabChange("attachment");
};
const handleAttachmentCreate = (_data: { file?: File; title: string; linkId: string }) => {
handleAttachmentClose();
const handleAttachmentCreate = async (data: { file?: File; title: string; linkId: string }) => {
const effectiveTaskId = taskId || taskDetail?.id;
if (!effectiveTaskId) return;
let fileUrl = data.linkId.trim();
if (data.file) {
const formData = new FormData();
formData.append("file", data.file);
try {
const uploadResult = await singleUpload.mutateAsync(formData);
fileUrl = uploadResult?.data?.url ?? "";
} catch (error) {
toast.error((error as ErrorType).response?.data?.error.message[0]);
return;
}
}
if (!fileUrl) return;
const title = data.title.trim() || data.file?.name || "";
const type = data.file ? "file" : "link";
createTaskAttachment.mutate(
{ file: fileUrl, title, type, taskId: effectiveTaskId },
{
onSuccess: () => {
handleAttachmentClose();
toast.success(t("success"));
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0]);
},
},
);
};
const isAttachmentSubmitting = singleUpload.isPending || createTaskAttachment.isPending;
const handleDateClose = () => {
if (isDateOpen) onTabChange("date");
};
@@ -258,7 +297,11 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
}
attachmentPopover={
isAttachmentOpen ? (
<TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} />
<TaskDetailAttachmentPopover
onClose={handleAttachmentClose}
onSubmit={handleAttachmentCreate}
isSubmitting={isAttachmentSubmitting}
/>
) : null
}
datePopover={
@@ -5,7 +5,7 @@ import { timeAgo } from "../../../../../config/func";
type Props = {
title: string;
createdAt: string;
createdAt?: string;
type: "file" | "link";
url?: string;
onEdit: () => void;
@@ -21,14 +21,14 @@ const AttachmentItem: FC<Props> = ({ title, createdAt, type, url, onEdit, onDele
</div>
<div className="min-w-0">
{type === "link" && url ? (
{url ? (
<a href={url} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-[#0047FF] underline truncate block">
{title}
</a>
) : (
<div className="text-xs font-bold truncate">{title}</div>
)}
<div className="text-[11px] mt-0.5">{timeAgo(createdAt)}</div>
{createdAt && <div className="text-[11px] mt-0.5">{timeAgo(createdAt)}</div>}
</div>
</div>
@@ -14,13 +14,13 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({
id: item.id,
title: item.title,
fileName: item.fileName,
url: item.url,
type: item.type,
file: item.file,
createdAt: item.createdAt,
}));
const files = mappedAttachments.filter((item) => item.fileName);
const links = mappedAttachments.filter((item) => item.url);
const files = mappedAttachments.filter((item) => item.type === "file");
const links = mappedAttachments.filter((item) => item.type === "link");
if (mappedAttachments.length === 0) return null;
@@ -36,9 +36,10 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
{files.map((item) => (
<AttachmentItem
key={item.id}
title={item.fileName ?? item.title}
createdAt={item.createdAt ?? ""}
title={item.title}
createdAt={item.createdAt}
type="file"
url={item.file}
onEdit={() => {}}
onDelete={() => {}}
/>
@@ -58,7 +59,7 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
title={item.title}
createdAt={item.createdAt}
type="link"
url={item.url}
url={item.file}
onEdit={() => {}}
onDelete={() => {}}
/>
@@ -7,9 +7,10 @@ import Select from "../../../../../components/Select";
type Props = {
onClose: () => void;
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
isSubmitting?: boolean;
};
const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit }) => {
const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit, isSubmitting = false }) => {
const { t } = useTranslation("global");
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
@@ -64,10 +65,10 @@ const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit }) => {
/>
<div className="flex justify-end gap-2">
<Button type="button" onClick={onClose} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
<Button type="button" onClick={onClose} disabled={isSubmitting} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
{t("cancel")}
</Button>
<Button type="button" onClick={handleSubmit} disabled={!canSubmit} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
<Button type="button" onClick={handleSubmit} disabled={!canSubmit || isSubmitting} isLoading={isSubmitting} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
{t("taskmanager.task_detail.insert")}
</Button>
</div>
@@ -4,10 +4,11 @@ import TaskDetailAttachmentForm from "./TaskDetailAttachmentForm";
type Props = {
onClose: () => void;
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
isSubmitting?: boolean;
};
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => {
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />;
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit, isSubmitting }) => {
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} isSubmitting={isSubmitting} />;
};
export default TaskDetailAttachmentPopover;
@@ -1,8 +1,7 @@
export type TaskAttachment = {
id: string;
title: string;
createdAt: string;
linkId?: string;
fileName?: string;
url?: string;
type: "file" | "link";
file?: string;
createdAt?: string;
};
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/TaskService";
import {
CreateCheckListItemType,
CreateTaskAttachmentType,
CreateTaskRemarkType,
CreateTaskType,
UpdateCheckListItemType,
@@ -88,3 +89,15 @@ export const useUpdateCheckListItem = () => {
},
});
};
export const useCreateTaskAttachment = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: CreateTaskAttachmentType) => api.createTaskAttachment(variables),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
queryClient.invalidateQueries({ queryKey: ["project"] });
},
});
};
@@ -2,6 +2,7 @@ import axios from "../../../../config/axios";
import {
CreateCheckListItemResponse,
CreateCheckListItemType,
CreateTaskAttachmentType,
CreateTaskRemarkResponse,
CreateTaskRemarkType,
CreateTaskType,
@@ -48,3 +49,8 @@ export const updateCheckListItem = async (
const { data } = await axios.patch(`/task-manager/check-list-items/${id}`, params);
return data;
};
export const createTaskAttachment = async (params: CreateTaskAttachmentType) => {
const { data } = await axios.post(`/task-manager/attachments`, params);
return data;
};
@@ -74,11 +74,18 @@ export type TaskChecklistType = {
export type TaskAttachmentType = {
id: string;
title: string;
fileName?: string;
url?: string;
type: "file" | "link";
file?: string;
createdAt?: string;
};
export type CreateTaskAttachmentType = {
file: string;
title: string;
type: "file" | "link";
taskId: string;
};
export type TaskDetailType = {
id: string;
createdAt: string;