attachment
This commit is contained in:
@@ -17,7 +17,8 @@ import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
|||||||
import type { TaskDetailTab } from "./types";
|
import type { TaskDetailTab } from "./types";
|
||||||
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
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";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -37,6 +38,8 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
const createCheckListItem = useCreateCheckListItem();
|
const createCheckListItem = useCreateCheckListItem();
|
||||||
|
const createTaskAttachment = useCreateTaskAttachment();
|
||||||
|
const singleUpload = useSingleUpload();
|
||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
const getUsers = useGetUsers("");
|
const getUsers = useGetUsers("");
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
@@ -204,10 +207,46 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
if (isAttachmentOpen) onTabChange("attachment");
|
if (isAttachmentOpen) onTabChange("attachment");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAttachmentCreate = (_data: { file?: File; title: string; linkId: string }) => {
|
const handleAttachmentCreate = async (data: { file?: File; title: string; linkId: string }) => {
|
||||||
handleAttachmentClose();
|
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 = () => {
|
const handleDateClose = () => {
|
||||||
if (isDateOpen) onTabChange("date");
|
if (isDateOpen) onTabChange("date");
|
||||||
};
|
};
|
||||||
@@ -258,7 +297,11 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
}
|
}
|
||||||
attachmentPopover={
|
attachmentPopover={
|
||||||
isAttachmentOpen ? (
|
isAttachmentOpen ? (
|
||||||
<TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} />
|
<TaskDetailAttachmentPopover
|
||||||
|
onClose={handleAttachmentClose}
|
||||||
|
onSubmit={handleAttachmentCreate}
|
||||||
|
isSubmitting={isAttachmentSubmitting}
|
||||||
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
datePopover={
|
datePopover={
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { timeAgo } from "../../../../../config/func";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
createdAt: string;
|
createdAt?: string;
|
||||||
type: "file" | "link";
|
type: "file" | "link";
|
||||||
url?: string;
|
url?: string;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -21,14 +21,14 @@ const AttachmentItem: FC<Props> = ({ title, createdAt, type, url, onEdit, onDele
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">
|
<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">
|
<a href={url} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-[#0047FF] underline truncate block">
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs font-bold truncate">{title}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({
|
const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
fileName: item.fileName,
|
type: item.type,
|
||||||
url: item.url,
|
file: item.file,
|
||||||
createdAt: item.createdAt,
|
createdAt: item.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const files = mappedAttachments.filter((item) => item.fileName);
|
const files = mappedAttachments.filter((item) => item.type === "file");
|
||||||
const links = mappedAttachments.filter((item) => item.url);
|
const links = mappedAttachments.filter((item) => item.type === "link");
|
||||||
|
|
||||||
if (mappedAttachments.length === 0) return null;
|
if (mappedAttachments.length === 0) return null;
|
||||||
|
|
||||||
@@ -36,9 +36,10 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
{files.map((item) => (
|
{files.map((item) => (
|
||||||
<AttachmentItem
|
<AttachmentItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
title={item.fileName ?? item.title}
|
title={item.title}
|
||||||
createdAt={item.createdAt ?? ""}
|
createdAt={item.createdAt}
|
||||||
type="file"
|
type="file"
|
||||||
|
url={item.file}
|
||||||
onEdit={() => {}}
|
onEdit={() => {}}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
/>
|
/>
|
||||||
@@ -58,7 +59,7 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
title={item.title}
|
title={item.title}
|
||||||
createdAt={item.createdAt}
|
createdAt={item.createdAt}
|
||||||
type="link"
|
type="link"
|
||||||
url={item.url}
|
url={item.file}
|
||||||
onEdit={() => {}}
|
onEdit={() => {}}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+4
-3
@@ -7,9 +7,10 @@ import Select from "../../../../../components/Select";
|
|||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: { file?: File; title: string; linkId: string }) => 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 { t } = useTranslation("global");
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [file, setFile] = useState<File | null>(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">
|
<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")}
|
{t("cancel")}
|
||||||
</Button>
|
</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")}
|
{t("taskmanager.task_detail.insert")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,11 @@ import TaskDetailAttachmentForm from "./TaskDetailAttachmentForm";
|
|||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
|
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
|
||||||
|
isSubmitting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => {
|
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit, isSubmitting }) => {
|
||||||
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />;
|
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} isSubmitting={isSubmitting} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskDetailAttachmentPopover;
|
export default TaskDetailAttachmentPopover;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
export type TaskAttachment = {
|
export type TaskAttachment = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
createdAt: string;
|
type: "file" | "link";
|
||||||
linkId?: string;
|
file?: string;
|
||||||
fileName?: string;
|
createdAt?: string;
|
||||||
url?: string;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import * as api from "../service/TaskService";
|
import * as api from "../service/TaskService";
|
||||||
import {
|
import {
|
||||||
CreateCheckListItemType,
|
CreateCheckListItemType,
|
||||||
|
CreateTaskAttachmentType,
|
||||||
CreateTaskRemarkType,
|
CreateTaskRemarkType,
|
||||||
CreateTaskType,
|
CreateTaskType,
|
||||||
UpdateCheckListItemType,
|
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 {
|
import {
|
||||||
CreateCheckListItemResponse,
|
CreateCheckListItemResponse,
|
||||||
CreateCheckListItemType,
|
CreateCheckListItemType,
|
||||||
|
CreateTaskAttachmentType,
|
||||||
CreateTaskRemarkResponse,
|
CreateTaskRemarkResponse,
|
||||||
CreateTaskRemarkType,
|
CreateTaskRemarkType,
|
||||||
CreateTaskType,
|
CreateTaskType,
|
||||||
@@ -48,3 +49,8 @@ export const updateCheckListItem = async (
|
|||||||
const { data } = await axios.patch(`/task-manager/check-list-items/${id}`, params);
|
const { data } = await axios.patch(`/task-manager/check-list-items/${id}`, params);
|
||||||
return data;
|
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 = {
|
export type TaskAttachmentType = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
fileName?: string;
|
type: "file" | "link";
|
||||||
url?: string;
|
file?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateTaskAttachmentType = {
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
type: "file" | "link";
|
||||||
|
taskId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TaskDetailType = {
|
export type TaskDetailType = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user