feat: document operation
This commit is contained in:
229
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
229
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { useRef, useState, useEffect, ChangeEvent } from "react";
|
||||||
|
import {
|
||||||
|
ArrowDownTrayIcon,
|
||||||
|
ArrowUpTrayIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import api from "../../utils/axios";
|
||||||
|
import { useBackdropStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { useToast } from "../../hooks/useToast";
|
||||||
|
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||||
|
import { RolesContextMenu } from "../Button/RolesContextMenu";
|
||||||
|
|
||||||
|
interface DocumentOperationProps {
|
||||||
|
downloadLink: string;
|
||||||
|
uploadLink: string;
|
||||||
|
validFiles?: string[];
|
||||||
|
payloadKey: string;
|
||||||
|
onUploadSuccess?: () => void;
|
||||||
|
page?: string;
|
||||||
|
access?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildAcceptString = (extensions: string[]): string => {
|
||||||
|
const mimeTypes: string[] = [];
|
||||||
|
|
||||||
|
extensions.forEach((ext) => {
|
||||||
|
const lower = ext.toLowerCase().replace(".", "");
|
||||||
|
|
||||||
|
if (lower === "img" || lower === "image") {
|
||||||
|
mimeTypes.push("image/*");
|
||||||
|
} else {
|
||||||
|
mimeTypes.push(`.${lower}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mimeTypes.join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentOperation = ({
|
||||||
|
downloadLink,
|
||||||
|
uploadLink,
|
||||||
|
validFiles = [],
|
||||||
|
payloadKey,
|
||||||
|
onUploadSuccess,
|
||||||
|
page = "",
|
||||||
|
access = "",
|
||||||
|
}: DocumentOperationProps) => {
|
||||||
|
const { openBackdrop, closeBackdrop } = useBackdropStore();
|
||||||
|
const showToast = useToast();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [uploadedFileName, setUploadedFileName] = useState<string>("");
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const isAdmin = profile?.role?.type?.key === "ADM";
|
||||||
|
|
||||||
|
const ableToSee = () => {
|
||||||
|
if (!access || !page) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const found = profile?.permissions?.find(
|
||||||
|
(item: any) => item.page_name === page,
|
||||||
|
);
|
||||||
|
if (found && found.page_access.includes(access)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (isAdmin && page && access) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setContextMenu({
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = () => {
|
||||||
|
if (contextMenu) {
|
||||||
|
setContextMenu(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contextMenu) {
|
||||||
|
document.addEventListener("click", handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClick);
|
||||||
|
};
|
||||||
|
}, [contextMenu]);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!downloadLink) return;
|
||||||
|
|
||||||
|
openBackdrop();
|
||||||
|
try {
|
||||||
|
const response = await api.get(downloadLink, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDisposition = response.headers["content-disposition"];
|
||||||
|
let fileName = "document";
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
const match = contentDisposition.match(
|
||||||
|
/filename\*?=(?:UTF-8''|"?)([^";]+)/i,
|
||||||
|
);
|
||||||
|
if (match?.[1]) {
|
||||||
|
fileName = decodeURIComponent(match[1].replace(/"/g, ""));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const urlParts = downloadLink.split("/").filter(Boolean);
|
||||||
|
const lastPart = urlParts[urlParts.length - 1];
|
||||||
|
if (lastPart && lastPart.includes(".")) {
|
||||||
|
fileName = lastPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute("download", fileName);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showToast("فایل با موفقیت دانلود شد", "success");
|
||||||
|
} catch {
|
||||||
|
showToast("خطا در دانلود فایل", "error");
|
||||||
|
} finally {
|
||||||
|
closeBackdrop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
openBackdrop();
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(payloadKey, file);
|
||||||
|
|
||||||
|
await api.post(uploadLink, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setUploadedFileName(file.name);
|
||||||
|
showToast("فایل با موفقیت آپلود شد", "success");
|
||||||
|
onUploadSuccess?.();
|
||||||
|
} catch {
|
||||||
|
showToast("خطا در آپلود فایل", "error");
|
||||||
|
} finally {
|
||||||
|
closeBackdrop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptString =
|
||||||
|
validFiles.length > 0 ? buildAcceptString(validFiles) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center"
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
accept={acceptString}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={!downloadLink || !ableToSee()}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-r-lg border border-l-0 border-primary-300 dark:border-dark-400 bg-primary-50 dark:bg-dark-600 text-primary-700 dark:text-primary-200 hover:bg-primary-100 dark:hover:bg-dark-500 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon className="w-4 h-4" />
|
||||||
|
<span>دانلود</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
disabled={!uploadLink || !ableToSee()}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-l-lg border border-primary-300 dark:border-dark-400 bg-primary-600 dark:bg-primary-700 text-white hover:bg-primary-500 dark:hover:bg-primary-800 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{uploadedFileName ? (
|
||||||
|
<CheckCircleIcon className="w-4 h-4 text-green-300" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpTrayIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span>{uploadedFileName ? "آپلود شده" : "آپلود"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contextMenu && page && access && (
|
||||||
|
<RolesContextMenu
|
||||||
|
page={page}
|
||||||
|
access={access}
|
||||||
|
position={contextMenu}
|
||||||
|
onClose={() => setContextMenu(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user