feat: add list style for resource dashboard (#1389)

* stash: file upload

* feat: add style button

* feat: add style of list

* feat: add checkbox for list

* 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

* feat: support the style of button

* feat: beautify the switch ui

* chore: refactor the component

* chore: refactor the resource item dropdown

* feat: use memo to reduce unused computing in drop

* feat: use memo to reduce the calc of resource list

* chore:change name

* 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

* feat: support to save the state of style

* remove pnpm-lock

* merge main

* chore: simpify the statement

* fix: delete conflict marker

* feat: add i18n for select

* feat:support dark mode

* eslint

* feat: delete the storage of resource style

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
CorrectRoadH 2023-03-26 19:32:53 +08:00 committed by GitHub
parent e84d562146
commit 7d89fcc892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 279 additions and 110 deletions

View File

@ -1,74 +1,26 @@
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
import { useResourceStore } from "../store/module";
import Icon from "./Icon";
import copy from "copy-to-clipboard";
import { getResourceUrl } from "../utils/resource";
import showPreviewImageDialog from "./PreviewImageDialog";
import Dropdown from "./base/Dropdown";
import ResourceCover from "./ResourceCover";
import { showCommonDialog } from "./Dialog/CommonDialog";
import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog";
import "../less/resource-card.less";
import ResourceItemDropdown from "./ResourceItemDropdown";
interface ResourceProps {
resource: Resource;
handlecheckClick: () => void;
handleUncheckClick: () => void;
}
const ResourceCard = ({ resource, handlecheckClick, handleUncheckClick }: ResourceProps) => {
const ResourceCard = ({
resource,
handleCheckClick,
handleUncheckClick,
handlePreviewBtnClick,
handleCopyResourceLinkBtnClick,
handleRenameBtnClick,
handleDeleteResourceBtnClick,
}: ResourceItemType) => {
const [isSelected, setIsSelected] = useState<boolean>(false);
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const { t } = useTranslation();
const handleRenameBtnClick = (resource: Resource) => {
showChangeResourceFilenameDialog(resource.id, resource.filename);
};
const handleDeleteResourceBtnClick = (resource: Resource) => {
let warningText = t("resources.warning-text");
if (resource.linkedMemoAmount > 0) {
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-resource-dialog",
onConfirm: async () => {
await resourceStore.deleteResourceById(resource.id);
},
});
};
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
const url = getResourceUrl(resource);
copy(url);
toast.success(t("message.succeed-copy-resource-link"));
};
const handleSelectBtnClick = () => {
if (isSelected) {
handleUncheckClick();
} else {
handlecheckClick();
handleCheckClick();
}
setIsSelected(!isSelected);
};
@ -79,40 +31,15 @@ const ResourceCard = ({ resource, handlecheckClick, handleUncheckClick }: Resour
<div onClick={() => handleSelectBtnClick()}>
{isSelected ? <Icon.CheckCircle2 className="resource-checkbox !flex" /> : <Icon.Circle className="resource-checkbox" />}
</div>
<Dropdown
className="more-action-btn"
actionsClassName="!w-28"
trigger={<Icon.MoreVertical className="w-4 h-auto hover:opacity-80 cursor-pointer" />}
actions={
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleCopyResourceLinkBtnClick(resource)}
>
{t("resources.copy-link")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleRenameBtnClick(resource)}
>
{t("resources.rename")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteResourceBtnClick(resource)}
>
{t("common.delete")}
</button>
</>
}
/>
<div className="more-action-btn">
<ResourceItemDropdown
resource={resource}
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
handlePreviewBtnClick={handlePreviewBtnClick}
handleRenameBtnClick={handleRenameBtnClick}
/>
</div>
</div>
<div className="w-full flex flex-row justify-center items-center pb-2 pt-4 px-2">
<ResourceCover resource={resource} />

View File

@ -0,0 +1,46 @@
import { useState } from "react";
import ResourceItemDropdown from "./ResourceItemDropdown";
const ResourceItem = ({
resource,
handleCheckClick,
handleUncheckClick,
handlePreviewBtnClick,
handleCopyResourceLinkBtnClick,
handleRenameBtnClick,
handleDeleteResourceBtnClick,
}: ResourceItemType) => {
const [isSelected, setIsSelected] = useState<boolean>(false);
const handleSelectBtnClick = () => {
if (isSelected) {
handleUncheckClick();
} else {
handleCheckClick();
}
setIsSelected(!isSelected);
};
return (
<div key={resource.id} className="px-2 py-2 w-full grid grid-cols-7">
<span className="w-full m-auto truncate col-span-1 justify-center">
<input type="checkbox" onClick={handleSelectBtnClick}></input>
</span>
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-1">{resource.id}</span>
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-4" onClick={() => handleRenameBtnClick(resource)}>
{resource.filename}
</span>
<div className="w-full flex flex-row justify-between items-center mb-2">
<ResourceItemDropdown
resource={resource}
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
handlePreviewBtnClick={handlePreviewBtnClick}
handleRenameBtnClick={handleRenameBtnClick}
/>
</div>
</div>
);
};
export default ResourceItem;

View File

@ -0,0 +1,59 @@
import React from "react";
import { useTranslation } from "react-i18next";
import Dropdown from "./base/Dropdown";
import Icon from "./Icon";
interface ResourceItemDropdown {
resource: Resource;
handleRenameBtnClick: (resource: Resource) => void;
handleDeleteResourceBtnClick: (resource: Resource) => void;
handlePreviewBtnClick: (resource: Resource) => void;
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
}
const ResourceItemDropdown = ({
resource,
handlePreviewBtnClick,
handleCopyResourceLinkBtnClick,
handleRenameBtnClick,
handleDeleteResourceBtnClick,
}: ResourceItemDropdown) => {
const { t } = useTranslation();
return (
<Dropdown
actionsClassName="!w-28"
trigger={<Icon.MoreVertical className="w-4 h-auto hover:opacity-80 cursor-pointer" />}
actions={
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleCopyResourceLinkBtnClick(resource)}
>
{t("resources.copy-link")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleRenameBtnClick(resource)}
>
{t("resources.rename")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteResourceBtnClick(resource)}
>
{t("common.delete")}
</button>
</>
}
/>
);
};
export default React.memo(ResourceItemDropdown);

View File

@ -0,0 +1,17 @@
import { useState } from "react";
const useListStyle = () => {
// true is Table Style, false is Grid Style
const [listStyle, setListStyle] = useState(false);
return {
listStyle: listStyle,
setToTableStyle: () => {
setListStyle(true);
},
setToGridStyle: () => {
setListStyle(false);
},
};
};
export default useListStyle;

View File

@ -81,7 +81,8 @@
"delete-selected-resources": "Delete Selected Resources",
"no-files-selected": "No files selected❗",
"upload-successfully": "Upload successfully",
"file-drag-drop-prompt": "Drag and drop your file here to upload file"
"file-drag-drop-prompt": "Drag and drop your file here to upload file",
"select": "Select"
},
"archived": {
"archived-memos": "Archived Memos",

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Button } from "@mui/joy";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
@ -7,10 +7,16 @@ import { useResourceStore } from "../store/module";
import Icon from "../components/Icon";
import ResourceCard from "../components/ResourceCard";
import ResourceSearchBar from "../components/ResourceSearchBar";
import { showCommonDialog } from "../components/Dialog/CommonDialog";
import showCreateResourceDialog from "../components/CreateResourceDialog";
import MobileHeader from "../components/MobileHeader";
import Dropdown from "../components/base/Dropdown";
import ResourceItem from "../components/ResourceItem";
import { showCommonDialog } from "../components/Dialog/CommonDialog";
import showChangeResourceFilenameDialog from "../components/ChangeResourceFilenameDialog";
import copy from "copy-to-clipboard";
import { getResourceUrl } from "../utils/resource";
import showPreviewImageDialog from "../components/PreviewImageDialog";
import showCreateResourceDialog from "../components/CreateResourceDialog";
import useListStyle from "../hooks/useListStyle";
const ResourcesDashboard = () => {
const { t } = useTranslation();
@ -20,6 +26,7 @@ const ResourcesDashboard = () => {
const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]);
const [isVisible, setIsVisible] = useState<boolean>(false);
const [queryText, setQueryText] = useState<string>("");
const { listStyle, setToTableStyle, setToGridStyle } = useListStyle();
const [dragActive, setDragActive] = useState(false);
useEffect(() => {
@ -95,6 +102,86 @@ const ResourcesDashboard = () => {
}
};
const handleStyleChangeBtnClick = (listStyleValue: boolean) => {
if (listStyleValue) {
setToTableStyle();
} else {
setToGridStyle();
}
setSelectedList([]);
};
const handleRenameBtnClick = (resource: Resource) => {
showChangeResourceFilenameDialog(resource.id, resource.filename);
};
const handleDeleteResourceBtnClick = (resource: Resource) => {
let warningText = t("resources.warning-text");
if (resource.linkedMemoAmount > 0) {
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-resource-dialog",
onConfirm: async () => {
await resourceStore.deleteResourceById(resource.id);
},
});
};
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
const url = getResourceUrl(resource);
copy(url);
toast.success(t("message.succeed-copy-resource-link"));
};
const resourceList = useMemo(
() =>
resources
.filter((res: Resource) => (queryText === "" ? true : res.filename.toLowerCase().includes(queryText.toLowerCase())))
.map((resource) =>
listStyle ? (
<ResourceItem
key={resource.id}
resource={resource}
handleCheckClick={() => handleCheckBtnClick(resource.id)}
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
handleRenameBtnClick={handleRenameBtnClick}
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
handlePreviewBtnClick={handlePreviewBtnClick}
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
></ResourceItem>
) : (
<ResourceCard
key={resource.id}
resource={resource}
handleCheckClick={() => handleCheckBtnClick(resource.id)}
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
handleRenameBtnClick={handleRenameBtnClick}
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
handlePreviewBtnClick={handlePreviewBtnClick}
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
></ResourceCard>
)
),
[resources, queryText, listStyle]
);
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
@ -157,6 +244,20 @@ const ResourcesDashboard = () => {
<Button onClick={() => showCreateResourceDialog({})}>
<Icon.Plus className="w-4 h-auto" />
</Button>
<div className="flex">
<div
className={`rounded-l-lg p-2 ${listStyle ? "bg-gray-200 dark:bg-zinc-800" : "bg-white bg-zinc-700"}`}
onClick={() => handleStyleChangeBtnClick(true)}
>
<Icon.List />
</div>
<div
className={`rounded-r-lg p-2 ${listStyle ? "bg-white bg-zinc-700" : "bg-gray-200 dark:bg-zinc-800"}`}
onClick={() => handleStyleChangeBtnClick(false)}
>
<Icon.Grid />
</div>
</div>
<Dropdown
className="drop-shadow-none"
actionsClassName="!w-28 rounded-lg drop-shadow-md dark:bg-zinc-800"
@ -185,20 +286,25 @@ const ResourcesDashboard = () => {
<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">
<div
className={
listStyle
? "flex flex-col justify-start items-start w-full"
: "w-full h-auto grid grid-cols-2 md:grid-cols-4 md:px-6 gap-6"
}
>
{listStyle && (
<div className="px-2 py-2 w-full grid grid-cols-7 border-b dark:border-b-zinc-600">
<span>{t("resources.select")}</span>
<span className="field-text id-text">ID</span>
<span className="field-text name-text">{t("resources.name")}</span>
<span></span>
</div>
)}
{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>
))
resourceList
)}
</div>
)}

11
web/src/types/resourceItem.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
interface ResourceProps {
resource: Resource;
handleCheckClick: () => void;
handleUncheckClick: () => void;
handleRenameBtnClick: (resource: Resource) => void;
handleDeleteResourceBtnClick: (resource: Resource) => void;
handlePreviewBtnClick: (resource: Resource) => void;
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
}
type ResourceItemType = ResourceProps;