feat: support resources reuse (#620)

* feat: support resource reuse

* update

* update

* update

* update
This commit is contained in:
Zeng1998 2022-11-29 19:07:20 +08:00 committed by GitHub
parent eba23c4f6e
commit ab8e3473a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 312 additions and 52 deletions

View File

@ -13,6 +13,7 @@ import Selector from "./common/Selector";
import Editor, { EditorRefActions } from "./Editor/Editor";
import EmojiPicker from "./Editor/EmojiPicker";
import ResourceIcon from "./ResourceIcon";
import showResourcesSelectorDialog from "./ResourcesSelectorDialog";
import "../less/memo-editor.less";
const getEditorContentCache = (): string => {
@ -34,7 +35,6 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => {
interface State {
fullscreen: boolean;
isUploadingResource: boolean;
resourceList: Resource[];
shouldShowEmojiPicker: boolean;
}
@ -48,7 +48,6 @@ const MemoEditor = () => {
isUploadingResource: false,
fullscreen: false,
shouldShowEmojiPicker: false,
resourceList: [],
});
const [allowSave, setAllowSave] = useState<boolean>(false);
const prevGlobalStateRef = useRef(editorState);
@ -79,13 +78,8 @@ const MemoEditor = () => {
if (memo) {
handleEditorFocus();
editorStateService.setMemoVisibility(memo.visibility);
editorStateService.setResourceList(memo.resourceList);
editorRef.current?.setContent(memo.content ?? "");
setState((state) => {
return {
...state,
resourceList: memo.resourceList,
};
});
}
});
storage.set({
@ -148,12 +142,7 @@ const MemoEditor = () => {
}
}
}
setState((state) => {
return {
...state,
resourceList: [...state.resourceList, ...resourceList],
};
});
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
}
};
@ -163,12 +152,7 @@ const MemoEditor = () => {
const file = event.clipboardData.files[0];
const resource = await handleUploadResource(file);
if (resource) {
setState((state) => {
return {
...state,
resourceList: [...state.resourceList, resource],
};
});
editorStateService.setResourceList([...editorState.resourceList, resource]);
}
}
};
@ -216,7 +200,7 @@ const MemoEditor = () => {
id: prevMemo.id,
content,
visibility: editorState.memoVisibility,
resourceIdList: state.resourceList.map((resource) => resource.id),
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
}
editorStateService.clearEditMemo();
@ -224,7 +208,7 @@ const MemoEditor = () => {
await memoService.createMemo({
content,
visibility: editorState.memoVisibility,
resourceIdList: state.resourceList.map((resource) => resource.id),
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
locationService.clearQuery();
}
@ -237,20 +221,17 @@ const MemoEditor = () => {
return {
...state,
fullscreen: false,
resourceList: [],
};
});
editorStateService.clearResourceList();
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
editorRef.current?.setContent("");
};
const handleCancelEdit = () => {
setState({
...state,
resourceList: [],
});
editorStateService.clearEditMemo();
editorStateService.clearResourceList();
editorRef.current?.setContent("");
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
@ -317,12 +298,7 @@ const MemoEditor = () => {
}
}
}
setState((state) => {
return {
...state,
resourceList: [...state.resourceList, ...resourceList],
};
});
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
document.body.removeChild(inputEl);
};
inputEl.click();
@ -361,13 +337,7 @@ const MemoEditor = () => {
};
const handleDeleteResource = async (resourceId: ResourceId) => {
setState((state) => {
return {
...state,
resourceList: state.resourceList.filter((resource) => resource.id !== resourceId),
};
});
editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
if (editorState.editMemoId) {
await deleteMemoResource(editorState.editMemoId, resourceId);
}
@ -440,10 +410,20 @@ const MemoEditor = () => {
<button className="action-btn">
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
</button>
<button className="action-btn">
<Icon.FileText className="icon-img" onClick={handleUploadFileBtnClick} />
<div className="action-btn resource-btn">
<Icon.FileText className="icon-img" />
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
</button>
<div className="resource-action-list">
<div className="resource-action-item" onClick={handleUploadFileBtnClick}>
<Icon.Upload className="icon-img" />
<span>{t("editor.local")}</span>
</div>
<div className="resource-action-item" onClick={showResourcesSelectorDialog}>
<Icon.Database className="icon-img" />
<span>{t("editor.resources")}</span>
</div>
</div>
</div>
<button className="action-btn" onClick={handleFullscreenBtnClick}>
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
</button>
@ -454,9 +434,9 @@ const MemoEditor = () => {
/>
</div>
</div>
{state.resourceList.length > 0 && (
{editorState.resourceList && editorState.resourceList.length > 0 && (
<div className="resource-list-wrapper">
{state.resourceList.map((resource) => {
{editorState.resourceList.map((resource) => {
return (
<div key={resource.id} className="resource-container">
<ResourceIcon resourceType="resource.type" className="icon-img" />

View File

@ -0,0 +1,155 @@
import { Checkbox, Tooltip } from "@mui/joy";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { editorStateService, resourceService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import toastHelper from "./Toast";
import { generateDialog } from "./Dialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import "../less/resources-selector-dialog.less";
type Props = DialogProps;
interface State {
checkedArray: boolean[];
}
const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const { resources } = useAppSelector((state) => state.resource);
const editorState = useAppSelector((state) => state.editor);
const [state, setState] = useState<State>({
checkedArray: [],
});
useEffect(() => {
resourceService
.fetchResourceList()
.catch((error) => {
console.error(error);
toastHelper.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, []);
useEffect(() => {
const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id);
setState({
checkedArray: resources.map((resource) => {
return checkedResourceIdArray.includes(resource.id);
}),
});
}, [resources]);
const getResourceUrl = useCallback((resource: Resource) => {
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
}, []);
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 handleCheckboxChange = (index: number) => {
const newCheckedArr = state.checkedArray;
newCheckedArr[index] = !newCheckedArr[index];
setState({
checkedArray: newCheckedArr,
});
};
const handleConfirmBtnClick = () => {
const resourceList = resources.filter((_, index) => {
return state.checkedArray[index];
});
editorStateService.setResourceList(resourceList);
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🌄</span>
{t("sidebar.resources")}
</p>
<button className="btn close-btn" onClick={destroy}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
{loadingState.isLoading ? (
<div className="loading-text-container">
<p className="tip-text">{t("resources.fetching-data")}</p>
</div>
) : (
<div className="resource-table-container">
<div className="fields-container">
<span className="field-text id-text">ID</span>
<span className="field-text name-text">NAME</span>
<span></span>
</div>
{resources.length === 0 ? (
<p className="tip-text">{t("resources.no-resources")}</p>
) : (
resources.map((resource, index) => (
<div key={resource.id} className="resource-container">
<span className="field-text id-text">{resource.id}</span>
<Tooltip title={resource.filename}>
<span className="field-text name-text">{resource.filename}</span>
</Tooltip>
<div className="flex justify-end">
<Icon.Eye
className=" text-left text-sm leading-6 px-1 mr-2 cursor-pointer hover:bg-gray-100"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</Icon.Eye>
<Checkbox checked={state.checkedArray[index]} onChange={() => handleCheckboxChange(index)} />
</div>
</div>
))
)}
</div>
)}
<div className="flex justify-between w-full mt-2 px-2">
<span className="text-sm font-mono text-gray-500 leading-8">
{t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length}
</span>
<div className="flex flex-row justify-start items-center">
<div
className="text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80"
onClick={handleConfirmBtnClick}
>
<Icon.PlusSquare className=" w-4 h-auto mr-1" />
<span>{t("common.confirm")}</span>
</div>
</div>
</div>
</div>
</>
);
};
export default function showResourcesSelectorDialog() {
generateDialog(
{
className: "resources-selector-dialog",
},
ResourcesSelectorDialog,
{}
);
}

View File

@ -67,6 +67,30 @@
}
}
&.resource-btn{
@apply relative ;
&:hover {
> .resource-action-list {
@apply flex;
}
}
>.resource-action-list{
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-36 max-h-52 overflow-auto font-mono bg-zinc-100;
>.resource-action-item{
@apply w-full flex text-black cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 shrink-0;
>.icon-img{
@apply w-4 mr-2;
}
}
}
}
> .icon-img {
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
}

View File

@ -0,0 +1,75 @@
.resources-selector-dialog {
@apply px-4;
> .dialog-container {
@apply w-112 max-w-full mb-8;
> .dialog-content-container {
@apply flex flex-col justify-start items-start w-full;
> .action-buttons-container {
@apply w-full flex flex-row justify-between items-center mb-2;
> .buttons-wrapper {
@apply flex flex-row justify-start items-center;
> .upload-resource-btn {
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80;
> .icon-img {
@apply w-4 h-auto mr-1;
}
}
> .delete-unused-resource-btn {
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-red-600 text-red-600 bg-red-100 hover:opacity-80;
> .icon-img {
@apply w-4 h-auto mr-1;
}
}
}
}
> .loading-text-container {
@apply flex flex-col justify-center items-center w-full h-32;
}
> .resource-table-container {
@apply flex flex-col justify-start items-start w-full;
> .fields-container {
@apply px-2 py-2 w-full grid grid-cols-7 border-b;
> .field-text {
@apply font-mono text-gray-400;
}
}
> .tip-text {
@apply w-full text-center text-base my-6 mt-8;
}
> .resource-container {
@apply px-2 py-2 w-full grid grid-cols-7;
> .buttons-container {
@apply w-full flex flex-row justify-end items-center;
}
}
.field-text {
@apply w-full truncate text-base pr-2 last:pr-0;
&.id-text {
@apply col-span-2;
}
&.name-text {
@apply col-span-4;
}
}
}
}
}
}

View File

@ -88,7 +88,9 @@
"save": "Save",
"placeholder": "Any thoughts...",
"only-image-supported": "Only image file supported.",
"cant-empty": "Content can't be empty"
"cant-empty": "Content can't be empty",
"local": "Local",
"resources": "Resources"
},
"memo": {
"view-detail": "View Detail",

View File

@ -88,7 +88,9 @@
"save": "Sauvegarder",
"placeholder": "Une idée...",
"only-image-supported": "Seul le fichier image est pris en charge.",
"cant-empty": "Le contenu ne doit pas être vide"
"cant-empty": "Le contenu ne doit pas être vide",
"local": "Local",
"resources": "Resources"
},
"memo": {
"view-detail": "Voir le détail",

View File

@ -88,7 +88,9 @@
"save": "Lưu",
"placeholder": "Bất cứ gì bạn đang nghĩ...",
"only-image-supported": "Chỉ hỗ trợ hình ảnh.",
"cant-empty": "Nội dung không thể trống"
"cant-empty": "Nội dung không thể trống",
"local": "Local",
"resources": "Resources"
},
"memo": {
"view-detail": "Xem chi tiết",

View File

@ -88,7 +88,9 @@
"save": "记下",
"placeholder": "现在的想法是...",
"only-image-supported": "仅支持图片文件。",
"cant-empty": "内容不能为空"
"cant-empty": "内容不能为空",
"local": "本地上传",
"resources": "资源库"
},
"memo": {
"view-detail": "查看详情",

View File

@ -1,5 +1,5 @@
import store from "../store";
import { setEditMemoId, setMemoVisibility } from "../store/modules/editor";
import { setEditMemoId, setMemoVisibility, setResourceList } from "../store/modules/editor";
const editorStateService = {
getState: () => {
@ -17,6 +17,14 @@ const editorStateService = {
setMemoVisibility: (memoVisibility: Visibility) => {
store.dispatch(setMemoVisibility(memoVisibility));
},
setResourceList: (resourceList: Resource[]) => {
store.dispatch(setResourceList(resourceList));
},
clearResourceList: () => {
store.dispatch(setResourceList([]));
},
};
export default editorStateService;

View File

@ -3,11 +3,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
editMemoId?: MemoId;
memoVisibility: Visibility;
resourceList: Resource[];
}
const editorSlice = createSlice({
name: "editor",
initialState: {} as State,
initialState: {
memoVisibility: "PRIVATE",
resourceList: [],
} as State,
reducers: {
setEditMemoId: (state, action: PayloadAction<Option<MemoId>>) => {
return {
@ -21,9 +25,15 @@ const editorSlice = createSlice({
memoVisibility: action.payload,
};
},
setResourceList: (state, action: PayloadAction<Resource[]>) => {
return {
...state,
resourceList: action.payload,
};
},
},
});
export const { setEditMemoId, setMemoVisibility } = editorSlice.actions;
export const { setEditMemoId, setMemoVisibility, setResourceList } = editorSlice.actions;
export default editorSlice.reducer;