feat: upload file by drag and drop (#1388)

* stash: file upload

* feat: support file upload by drag

* feat: beautify the ui

* feat: support file upload

* stash

* fix: the resource is incorrectly when upload multiple files

* feat: beautify the ui

* chore: reduce unused line

* stash

* chore: deleted unused line

* chore: deleted unused line

* chore

* chore: change the function declare

* feat: support to prompt file is too large

* feat:drop prompt to cover all element

* fix: eslint

* fix: the name of i18n

* chore: refactor the import deps

* feat: beautify the ui

* Update web/src/locales/en.json

Co-authored-by: boojack <stevenlgtm@gmail.com>

* chore: the import of  deps

* fix: the window size of fecting data

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
pull/1401/head
CorrectRoadH 2 years ago committed by GitHub
parent 026fb3e50e
commit 2ba54c9168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -79,7 +79,9 @@
"no-unused-resources": "No unused resources", "no-unused-resources": "No unused resources",
"name": "Name", "name": "Name",
"delete-selected-resources": "Delete Selected Resources", "delete-selected-resources": "Delete Selected Resources",
"no-files-selected": "No files selected❗" "no-files-selected": "No files selected❗",
"upload-successfully": "Upload successfully",
"file-drag-drop-prompt": "Drag and drop your file here to upload file"
}, },
"archived": { "archived": {
"archived-memos": "Archived Memos", "archived-memos": "Archived Memos",

@ -79,7 +79,9 @@
"no-unused-resources": "無可刪除的資源", "no-unused-resources": "無可刪除的資源",
"name": "資源名稱", "name": "資源名稱",
"delete-selected-resources": "刪除選中資源", "delete-selected-resources": "刪除選中資源",
"no-files-selected": "沒有文件被選中❗" "no-files-selected": "沒有文件被選中❗",
"upload-successfully": "上傳成功",
"file-drag-drop-prompt": "將您的文件拖放到此處以上傳文件"
}, },
"archived": { "archived": {
"archived-memos": "已封存的 Memo", "archived-memos": "已封存的 Memo",

@ -79,7 +79,9 @@
"no-unused-resources": "无可删除的资源", "no-unused-resources": "无可删除的资源",
"name": "资源名称", "name": "资源名称",
"delete-selected-resources": "删除选中资源", "delete-selected-resources": "删除选中资源",
"no-files-selected": "没有文件被选中❗" "no-files-selected": "没有文件被选中❗",
"upload-successfully": "上传成功",
"file-drag-drop-prompt": "将您的文件拖放到此处以上传文件"
}, },
"archived": { "archived": {
"archived-memos": "已归档的 Memo", "archived-memos": "已归档的 Memo",

@ -20,6 +20,7 @@ const ResourcesDashboard = () => {
const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]); const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]);
const [isVisible, setIsVisible] = useState<boolean>(false); const [isVisible, setIsVisible] = useState<boolean>(false);
const [queryText, setQueryText] = useState<string>(""); const [queryText, setQueryText] = useState<string>("");
const [dragActive, setDragActive] = useState(false);
useEffect(() => { useEffect(() => {
resourceStore resourceStore
@ -94,70 +95,114 @@ const ResourcesDashboard = () => {
} }
}; };
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
await resourceStore.createResourcesWithBlob(e.dataTransfer.files).then(
(res) => {
for (const resource of res) {
toast.success(`${resource.filename} ${t("resources.upload-successfully")}`);
}
},
(reason) => {
toast.error(reason);
}
);
}
};
return ( return (
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"> <section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<MobileHeader showSearch={false} /> <MobileHeader showSearch={false} />
<div className="w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300"> <div className="w-full relative" onDragEnter={handleDrag}>
<div className="relative w-full flex flex-row justify-between items-center"> {dragActive && (
<p className="flex flex-row justify-start items-center select-none rounded"> <div
<Icon.Paperclip className="w-5 h-auto mr-1" /> {t("common.resources")} className="absolute h-full w-full rounded-xl bg-zinc-800 dark:bg-white opacity-60 z-10"
</p> onDragEnter={handleDrag}
<ResourceSearchBar setQuery={setQueryText} /> onDragLeave={handleDrag}
</div> onDragOver={handleDrag}
<div className="w-full flex flex-row justify-end items-center space-x-2 mt-3 z-1"> onDrop={handleDrop}
{isVisible && ( >
<Button onClick={() => handleDeleteSelectedBtnClick()} color="danger"> <div className="flex h-full w-full">
<Icon.Trash2 className="w-4 h-auto" /> <p className="m-auto text-2xl text-white dark:text-black">{t("resources.file-drag-drop-prompt")}</p>
</Button>
)}
<Button onClick={() => showCreateResourceDialog({})}>
<Icon.Plus className="w-4 h-auto" />
</Button>
<Dropdown
className="drop-shadow-none"
actionsClassName="!w-28 rounded-lg drop-shadow-md dark:bg-zinc-800"
positionClassName="mt-2 top-full right-0"
trigger={
<Button variant="outlined">
<Icon.MoreVertical className="w-4 h-auto" />
</Button>
}
actions={
<>
<button
className="w-full flex flex-row justify-start items-center content-center text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={handleDeleteUnusedResourcesBtnClick}
>
<Icon.Trash2 className="w-4 h-auto mr-2" />
{t("resources.clear")}
</button>
</>
}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mt-4 mb-6">
{loadingState.isLoading ? (
<div className="w-full h-32 flex flex-col justify-center items-center">
<p className="w-full text-center text-base my-6 mt-8">{t("resources.fetching-data")}</p>
</div> </div>
) : ( </div>
<div className="w-full h-auto grid grid-cols-2 md:grid-cols-4 md:px-6 gap-6"> )}
{resources.length === 0 ? (
<p className="w-full text-center text-base my-6 mt-8">{t("resources.no-resources")}</p> <div className="w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
) : ( <div className="relative w-full flex flex-row justify-between items-center">
resources <p className="flex flex-row justify-start items-center select-none rounded">
.filter((res: Resource) => (queryText === "" ? true : res.filename.toLowerCase().includes(queryText.toLowerCase()))) <Icon.Paperclip className="w-5 h-auto mr-1" /> {t("common.resources")}
.map((resource) => ( </p>
<ResourceCard <ResourceSearchBar setQuery={setQueryText} />
key={resource.id} </div>
resource={resource} <div className="w-full flex flex-row justify-end items-center space-x-2 mt-3 z-1">
handlecheckClick={() => handleCheckBtnClick(resource.id)} {isVisible && (
handleUncheckClick={() => handleUncheckBtnClick(resource.id)} <Button onClick={() => handleDeleteSelectedBtnClick()} color="danger">
></ResourceCard> <Icon.Trash2 className="w-4 h-auto" />
)) </Button>
)} )}
</div> <Button onClick={() => showCreateResourceDialog({})}>
)} <Icon.Plus className="w-4 h-auto" />
</Button>
<Dropdown
className="drop-shadow-none"
actionsClassName="!w-28 rounded-lg drop-shadow-md dark:bg-zinc-800"
positionClassName="mt-2 top-full right-0"
trigger={
<Button variant="outlined">
<Icon.MoreVertical className="w-4 h-auto" />
</Button>
}
actions={
<>
<button
className="w-full flex flex-row justify-start items-center content-center text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={handleDeleteUnusedResourcesBtnClick}
>
<Icon.Trash2 className="w-4 h-auto mr-2" />
{t("resources.clear")}
</button>
</>
}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mt-4 mb-6">
{loadingState.isLoading ? (
<div className="w-full h-32 flex flex-col justify-center items-center">
<p className="w-full text-center text-base my-6 mt-8">{t("resources.fetching-data")}</p>
</div>
) : (
<div className="w-full h-auto grid grid-cols-2 md:grid-cols-4 md:px-6 gap-6">
{resources.length === 0 ? (
<p className="w-full text-center text-base my-6 mt-8">{t("resources.no-resources")}</p>
) : (
resources
.filter((res: Resource) => (queryText === "" ? true : res.filename.toLowerCase().includes(queryText.toLowerCase())))
.map((resource) => (
<ResourceCard
key={resource.id}
resource={resource}
handlecheckClick={() => handleCheckBtnClick(resource.id)}
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
></ResourceCard>
))
)}
</div>
)}
</div>
</div> </div>
</div> </div>
</section> </section>

@ -47,6 +47,24 @@ export const useResourceStore = () => {
store.dispatch(setResources([resource, ...resourceList])); store.dispatch(setResources([resource, ...resourceList]));
return resource; return resource;
}, },
async createResourcesWithBlob(files: FileList): Promise<Array<Resource>> {
let newResourceList: Array<Resource> = [];
for (const file of files) {
const { name: filename, size } = file;
if (size > MAX_FILE_SIZE) {
return Promise.reject(`${filename} overload max size: 32MB`);
}
const formData = new FormData();
formData.append("file", file, filename);
const { data } = (await api.createResourceWithBlob(formData)).data;
const resource = convertResponseModelResource(data);
newResourceList = [resource, ...newResourceList];
}
const resourceList = state.resources;
store.dispatch(setResources([...newResourceList, ...resourceList]));
return newResourceList;
},
async deleteResourceById(id: ResourceId) { async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id); await api.deleteResourceById(id);
store.dispatch(deleteResource(id)); store.dispatch(deleteResource(id));

Loading…
Cancel
Save