mirror of
https://github.com/usememos/memos.git
synced 2024-11-24 06:35:24 +03:00
feat: memo relation part1 (#1677)
* feat: memo relation part1 * chore: update
This commit is contained in:
parent
ca5859296a
commit
a07d5d38d6
@ -160,8 +160,17 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
memo, err = s.Store.ComposeMemo(ctx, memo)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, resourceID := range memoPatch.ResourceIDList {
|
resourceIDList := make([]int, 0)
|
||||||
|
for _, resource := range memo.ResourceList {
|
||||||
|
resourceIDList = append(resourceIDList, resource.ID)
|
||||||
|
}
|
||||||
|
addedResourceIDList, removedResourceIDList := getIDListDiff(resourceIDList, memoPatch.ResourceIDList)
|
||||||
|
for _, resourceID := range addedResourceIDList {
|
||||||
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||||
MemoID: memo.ID,
|
MemoID: memo.ID,
|
||||||
ResourceID: resourceID,
|
ResourceID: resourceID,
|
||||||
@ -169,19 +178,47 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, resourceID := range removedResourceIDList {
|
||||||
|
if err := s.Store.DeleteMemoResource(ctx, &api.MemoResourceDelete{
|
||||||
|
MemoID: &memo.ID,
|
||||||
|
ResourceID: &resourceID,
|
||||||
|
}); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if s.Profile.IsDev() {
|
if s.Profile.IsDev() {
|
||||||
|
patchMemoRelationList := make([]*api.MemoRelation, 0)
|
||||||
for _, memoRelationUpsert := range memoPatch.RelationList {
|
for _, memoRelationUpsert := range memoPatch.RelationList {
|
||||||
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
|
patchMemoRelationList = append(patchMemoRelationList, &api.MemoRelation{
|
||||||
MemoID: memo.ID,
|
MemoID: memo.ID,
|
||||||
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
|
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
|
||||||
Type: store.MemoRelationType(memoRelationUpsert.Type),
|
Type: memoRelationUpsert.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memo.RelationList, patchMemoRelationList)
|
||||||
|
for _, memoRelation := range addedMemoRelationList {
|
||||||
|
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
|
||||||
|
MemoID: memo.ID,
|
||||||
|
RelatedMemoID: memoRelation.RelatedMemoID,
|
||||||
|
Type: store.MemoRelationType(memoRelation.Type),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, memoRelation := range removedMemoRelationList {
|
||||||
|
memoRelationType := store.MemoRelationType(memoRelation.Type)
|
||||||
|
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelationMessage{
|
||||||
|
MemoID: &memo.ID,
|
||||||
|
RelatedMemoID: &memoRelation.RelatedMemoID,
|
||||||
|
Type: &memoRelationType,
|
||||||
|
}); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After patching memo resources and relations, we need to re-compose it to get the latest data.
|
||||||
memo, err = s.Store.ComposeMemo(ctx, memo)
|
memo, err = s.Store.ComposeMemo(ctx, memo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
|
||||||
@ -452,3 +489,49 @@ func (s *Server) createMemoCreateActivity(c echo.Context, memo *api.Memo) error
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIDListDiff(oldList, newList []int) (addedList, removedList []int) {
|
||||||
|
oldMap := map[int]bool{}
|
||||||
|
for _, id := range oldList {
|
||||||
|
oldMap[id] = true
|
||||||
|
}
|
||||||
|
newMap := map[int]bool{}
|
||||||
|
for _, id := range newList {
|
||||||
|
newMap[id] = true
|
||||||
|
}
|
||||||
|
for id := range oldMap {
|
||||||
|
if !newMap[id] {
|
||||||
|
removedList = append(removedList, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range newMap {
|
||||||
|
if !oldMap[id] {
|
||||||
|
addedList = append(addedList, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addedList, removedList
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMemoRelationListDiff(oldList, newList []*api.MemoRelation) (addedList, removedList []*api.MemoRelation) {
|
||||||
|
oldMap := map[string]bool{}
|
||||||
|
for _, relation := range oldList {
|
||||||
|
oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
|
||||||
|
}
|
||||||
|
newMap := map[string]bool{}
|
||||||
|
for _, relation := range newList {
|
||||||
|
newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
|
||||||
|
}
|
||||||
|
for _, relation := range oldList {
|
||||||
|
key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
|
||||||
|
if !newMap[key] {
|
||||||
|
removedList = append(removedList, relation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, relation := range newList {
|
||||||
|
key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
|
||||||
|
if !oldMap[key] {
|
||||||
|
addedList = append(addedList, relation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addedList, removedList
|
||||||
|
}
|
||||||
|
@ -40,9 +40,11 @@ func (raw *memoRaw) toMemo() *api.Memo {
|
|||||||
UpdatedTs: raw.UpdatedTs,
|
UpdatedTs: raw.UpdatedTs,
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Content: raw.Content,
|
Content: raw.Content,
|
||||||
Visibility: raw.Visibility,
|
Visibility: raw.Visibility,
|
||||||
Pinned: raw.Pinned,
|
Pinned: raw.Pinned,
|
||||||
|
ResourceList: []*api.Resource{},
|
||||||
|
RelationList: []*api.MemoRelation{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ func (s *Store) ComposeMemoRelationList(ctx context.Context, memo *api.Memo) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memo.RelationList = []*api.MemoRelation{}
|
||||||
for _, memoRelation := range memoRelationList {
|
for _, memoRelation := range memoRelationList {
|
||||||
memo.RelationList = append(memo.RelationList, &api.MemoRelation{
|
memo.RelationList = append(memo.RelationList, &api.MemoRelation{
|
||||||
MemoID: memoRelation.MemoID,
|
MemoID: memoRelation.MemoID,
|
||||||
|
@ -98,7 +98,7 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex flex-row justify-start items-start pr-8 space-x-2">
|
<div className="w-full flex flex-row justify-start items-start pr-8 space-x-2">
|
||||||
<Icon.Bot className="mt-2 flex-shrink-0 mr-1 w-6 h-auto opacity-80" />
|
<Icon.Bot className="mt-2 shrink-0 mr-1 w-6 h-auto opacity-80" />
|
||||||
<div className="memo-content-wrapper !w-auto flex flex-col justify-start items-start shadow rounded-lg rounded-tl-none px-3 py-2 bg-gray-100 dark:bg-zinc-700">
|
<div className="memo-content-wrapper !w-auto flex flex-col justify-start items-start shadow rounded-lg rounded-tl-none px-3 py-2 bg-gray-100 dark:bg-zinc-700">
|
||||||
<div className="memo-content-text">{marked(message.content)}</div>
|
<div className="memo-content-text">{marked(message.content)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@ const Header = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed sm:sticky top-0 left-0 w-full sm:w-56 h-full flex-shrink-0 pointer-events-none sm:pointer-events-auto z-20 ${
|
className={`fixed sm:sticky top-0 left-0 w-full sm:w-56 h-full shrink-0 pointer-events-none sm:pointer-events-auto z-20 ${
|
||||||
showHeader && "pointer-events-auto"
|
showHeader && "pointer-events-auto"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -42,7 +42,7 @@ const HomeSidebar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed md:sticky top-0 left-0 w-full md:w-56 h-full flex-shrink-0 pointer-events-none md:pointer-events-auto z-10 ${
|
className={`fixed md:sticky top-0 left-0 w-full md:w-56 h-full shrink-0 pointer-events-none md:pointer-events-auto z-10 ${
|
||||||
showHomeSidebar && "pointer-events-auto"
|
showHomeSidebar && "pointer-events-auto"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { getRelativeTimeString } from "@/helpers/datetime";
|
import { isEqual, uniqWith } from "lodash-es";
|
||||||
import { memo, useEffect, useRef, useState } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useEditorStore, useFilterStore, useMemoStore, useUserStore } from "@/store/module";
|
import { useEditorStore, useFilterStore, useMemoStore, useUserStore } from "@/store/module";
|
||||||
|
import { getRelativeTimeString } from "@/helpers/datetime";
|
||||||
|
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||||
|
import { useMemoCacheStore } from "@/store/zustand";
|
||||||
import Tooltip from "./kit/Tooltip";
|
import Tooltip from "./kit/Tooltip";
|
||||||
import Divider from "./kit/Divider";
|
import Divider from "./kit/Divider";
|
||||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||||
@ -13,24 +16,34 @@ import MemoResources from "./MemoResources";
|
|||||||
import showShareMemo from "./ShareMemoDialog";
|
import showShareMemo from "./ShareMemoDialog";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
||||||
|
import MemoRelationListView from "./MemoRelationListView";
|
||||||
import "@/less/memo.less";
|
import "@/less/memo.less";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
showRelatedMemos?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo, readonly } = props;
|
const { memo, readonly, showRelatedMemos } = props;
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
|
const memoCacheStore = useMemoCacheStore();
|
||||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.createdTs));
|
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.createdTs));
|
||||||
|
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then((memoList) => {
|
||||||
|
setRelatedMemoList(uniqWith(memoList, isEqual));
|
||||||
|
});
|
||||||
|
}, [memo.relationList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalFlag: any = -1;
|
let intervalFlag: any = -1;
|
||||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||||
@ -178,71 +191,106 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMarkMemo = () => {
|
||||||
|
const relation: MemoRelation = {
|
||||||
|
memoId: UNKNOWN_ID,
|
||||||
|
relatedMemoId: memo.id,
|
||||||
|
type: "REFERENCE",
|
||||||
|
};
|
||||||
|
editorStore.setRelationList(uniqWith([...editorStore.state.relationList, relation], isEqual));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
|
<>
|
||||||
<div className="memo-top-wrapper">
|
<div
|
||||||
<div className="status-text-container">
|
className={`memo-wrapper ${"memos-" + memo.id} ${relatedMemoList.length > 0 && "pinned"} ${memo.pinned ? "pinned" : ""}`}
|
||||||
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
|
ref={memoContainerRef}
|
||||||
{createdTimeStr}
|
>
|
||||||
</Link>
|
<div className="memo-top-wrapper">
|
||||||
{isVisitorMode && (
|
<div className="status-text-container">
|
||||||
<Link className="name-text" to={`/u/${memo.creatorId}`}>
|
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
|
||||||
@{memo.creatorName}
|
{createdTimeStr}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
{isVisitorMode && (
|
||||||
</div>
|
<Link className="name-text" to={`/u/${memo.creatorId}`}>
|
||||||
{!isVisitorMode && (
|
@{memo.creatorName}
|
||||||
<div className="btns-container space-x-2">
|
</Link>
|
||||||
{memo.visibility !== "PRIVATE" && (
|
|
||||||
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} side="top">
|
|
||||||
<div onClick={() => handleMemoVisibilityClick(memo.visibility)}>
|
|
||||||
{memo.visibility === "PUBLIC" ? (
|
|
||||||
<Icon.Globe2 className="w-4 h-auto cursor-pointer rounded text-green-600" />
|
|
||||||
) : (
|
|
||||||
<Icon.Users className="w-4 h-auto cursor-pointer rounded text-gray-500 dark:text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
|
</div>
|
||||||
<span className="btn more-action-btn">
|
{!isVisitorMode && (
|
||||||
<Icon.MoreHorizontal className="icon-img" />
|
<div className="btns-container space-x-2">
|
||||||
</span>
|
{memo.visibility !== "PRIVATE" && (
|
||||||
<div className="more-action-btns-wrapper">
|
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} side="top">
|
||||||
<div className="more-action-btns-container min-w-[6em]">
|
<div onClick={() => handleMemoVisibilityClick(memo.visibility)}>
|
||||||
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
|
{memo.visibility === "PUBLIC" ? (
|
||||||
{memo.pinned ? <Icon.BookmarkMinus className="w-4 h-auto mr-2" /> : <Icon.BookmarkPlus className="w-4 h-auto mr-2" />}
|
<Icon.Globe2 className="w-4 h-auto cursor-pointer rounded text-green-600" />
|
||||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
) : (
|
||||||
</span>
|
<Icon.Users className="w-4 h-auto cursor-pointer rounded text-gray-500 dark:text-gray-400" />
|
||||||
<span className="btn" onClick={handleEditMemoClick}>
|
)}
|
||||||
<Icon.Edit3 className="w-4 h-auto mr-2" />
|
</div>
|
||||||
{t("common.edit")}
|
</Tooltip>
|
||||||
</span>
|
)}
|
||||||
<span className="btn" onClick={handleGenerateMemoImageBtnClick}>
|
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
|
||||||
<Icon.Share className="w-4 h-auto mr-2" />
|
<span className="btn more-action-btn">
|
||||||
{t("common.share")}
|
<Icon.MoreHorizontal className="icon-img" />
|
||||||
</span>
|
</span>
|
||||||
<Divider />
|
<div className="more-action-btns-wrapper">
|
||||||
<span className="btn text-orange-500" onClick={handleArchiveMemoClick}>
|
<div className="more-action-btns-container min-w-[6em]">
|
||||||
<Icon.Archive className="w-4 h-auto mr-2" />
|
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||||
{t("common.archive")}
|
{memo.pinned ? <Icon.BookmarkMinus className="w-4 h-auto mr-2" /> : <Icon.BookmarkPlus className="w-4 h-auto mr-2" />}
|
||||||
</span>
|
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||||
<span className="btn text-red-600" onClick={handleDeleteMemoClick}>
|
</span>
|
||||||
<Icon.Trash className="w-4 h-auto mr-2" />
|
<span className="btn" onClick={handleEditMemoClick}>
|
||||||
{t("common.delete")}
|
<Icon.Edit3 className="w-4 h-auto mr-2" />
|
||||||
</span>
|
{t("common.edit")}
|
||||||
|
</span>
|
||||||
|
<span className="btn" onClick={handleGenerateMemoImageBtnClick}>
|
||||||
|
<Icon.Share className="w-4 h-auto mr-2" />
|
||||||
|
{t("common.share")}
|
||||||
|
</span>
|
||||||
|
<span className="btn" onClick={handleMarkMemo}>
|
||||||
|
<Icon.Link className="w-4 h-auto mr-2" />
|
||||||
|
Mark
|
||||||
|
</span>
|
||||||
|
<Divider />
|
||||||
|
<span className="btn text-orange-500" onClick={handleArchiveMemoClick}>
|
||||||
|
<Icon.Archive className="w-4 h-auto mr-2" />
|
||||||
|
{t("common.archive")}
|
||||||
|
</span>
|
||||||
|
<span className="btn text-red-600" onClick={handleDeleteMemoClick}>
|
||||||
|
<Icon.Trash className="w-4 h-auto mr-2" />
|
||||||
|
{t("common.delete")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
<MemoContent
|
||||||
|
content={memo.content}
|
||||||
|
onMemoContentClick={handleMemoContentClick}
|
||||||
|
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||||
|
/>
|
||||||
|
<MemoResources resourceList={memo.resourceList} />
|
||||||
|
{!showRelatedMemos && <MemoRelationListView relationList={memo.relationList} />}
|
||||||
</div>
|
</div>
|
||||||
<MemoContent
|
|
||||||
content={memo.content}
|
{showRelatedMemos && relatedMemoList.length > 0 && (
|
||||||
onMemoContentClick={handleMemoContentClick}
|
<>
|
||||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
<p className="font-mono text-sm mt-4 mb-1 pl-4 opacity-60 flex flex-row items-center">
|
||||||
/>
|
<Icon.Link className="w-4 h-auto mr-1" />
|
||||||
<MemoResources resourceList={memo.resourceList} />
|
<span>Related memos</span>
|
||||||
</div>
|
</p>
|
||||||
|
{relatedMemoList.map((relatedMemo) => {
|
||||||
|
return (
|
||||||
|
<div key={relatedMemo.id} className="w-full">
|
||||||
|
<Memo memo={relatedMemo} readonly={readonly} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
60
web/src/components/MemoEditor/RelationListView.tsx
Normal file
60
web/src/components/MemoEditor/RelationListView.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useEditorStore } from "@/store/module";
|
||||||
|
import { useMemoCacheStore } from "@/store/zustand";
|
||||||
|
import Icon from "../Icon";
|
||||||
|
|
||||||
|
interface FormatedMemoRelation extends MemoRelation {
|
||||||
|
relatedMemo: Memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelationListView = () => {
|
||||||
|
const editorStore = useEditorStore();
|
||||||
|
const memoCacheStore = useMemoCacheStore();
|
||||||
|
const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]);
|
||||||
|
const relationList = editorStore.state.relationList;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRelatedMemoList = async () => {
|
||||||
|
const requests = relationList.map(async (relation) => {
|
||||||
|
const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId);
|
||||||
|
return {
|
||||||
|
...relation,
|
||||||
|
relatedMemo,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const list = await Promise.all(requests);
|
||||||
|
setFormatedMemoRelationList(list);
|
||||||
|
};
|
||||||
|
fetchRelatedMemoList();
|
||||||
|
}, [relationList]);
|
||||||
|
|
||||||
|
const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => {
|
||||||
|
const newRelationList = relationList.filter((relation) => relation.relatedMemoId !== memoRelation.relatedMemoId);
|
||||||
|
editorStore.setRelationList(newRelationList);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{formatedMemoRelationList.length > 0 && (
|
||||||
|
<div className="w-full flex flex-row gap-2 mt-2 flex-wrap">
|
||||||
|
{formatedMemoRelationList.map((memoRelation) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={memoRelation.relatedMemoId}
|
||||||
|
className="w-auto max-w-[50%] overflow-hidden flex flex-row justify-start items-center bg-gray-100 hover:bg-gray-200 rounded text-sm p-1 px-2 text-gray-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Icon.Link className="w-4 h-auto shrink-0" />
|
||||||
|
<span className="mx-1 max-w-full text-ellipsis font-mono whitespace-nowrap overflow-hidden">
|
||||||
|
{memoRelation.relatedMemo.content}
|
||||||
|
</span>
|
||||||
|
<Icon.X className="w-4 h-auto hover:opacity-80 shrink-0" onClick={() => handleDeleteRelation(memoRelation)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelationListView;
|
@ -1,5 +1,4 @@
|
|||||||
import { useEditorStore } from "@/store/module";
|
import { useEditorStore } from "@/store/module";
|
||||||
import { deleteMemoResource } from "@/helpers/api";
|
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import ResourceIcon from "../ResourceIcon";
|
import ResourceIcon from "../ResourceIcon";
|
||||||
|
|
||||||
@ -9,21 +8,21 @@ const ResourceListView = () => {
|
|||||||
|
|
||||||
const handleDeleteResource = async (resourceId: ResourceId) => {
|
const handleDeleteResource = async (resourceId: ResourceId) => {
|
||||||
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
|
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
|
||||||
if (editorState.editMemoId) {
|
|
||||||
await deleteMemoResource(editorState.editMemoId, resourceId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{editorState.resourceList && editorState.resourceList.length > 0 && (
|
{editorState.resourceList && editorState.resourceList.length > 0 && (
|
||||||
<div className="resource-list-wrapper">
|
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2">
|
||||||
{editorState.resourceList.map((resource) => {
|
{editorState.resourceList.map((resource) => {
|
||||||
return (
|
return (
|
||||||
<div key={resource.id} className="resource-container">
|
<div
|
||||||
<ResourceIcon resourceType="resource.type" className="icon-img" />
|
key={resource.id}
|
||||||
<span className="name-text">{resource.filename}</span>
|
className="max-w-full flex flex-row justify-start items-center flex-nowrap bg-gray-100 px-2 py-1 rounded cursor-pointer text-gray-500 hover:bg-gray-200 dark:opacity-60"
|
||||||
<Icon.X className="close-icon" onClick={() => handleDeleteResource(resource.id)} />
|
>
|
||||||
|
<ResourceIcon resourceType={resource.type} className="w-4 h-auto mr-1" />
|
||||||
|
<span className="text-sm max-w-xs truncate font-mono">{resource.filename}</span>
|
||||||
|
<Icon.X className="w-4 h-auto ml-1 hover:opacity-80" onClick={() => handleDeleteResource(resource.id)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -32,4 +31,5 @@ const ResourceListView = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResourceListView;
|
export default ResourceListView;
|
||||||
|
@ -13,8 +13,9 @@ import Editor, { EditorRefActions } from "./Editor";
|
|||||||
import TagSelector from "./ActionButton/TagSelector";
|
import TagSelector from "./ActionButton/TagSelector";
|
||||||
import ResourceSelector from "./ActionButton/ResourceSelector";
|
import ResourceSelector from "./ActionButton/ResourceSelector";
|
||||||
import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector";
|
import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector";
|
||||||
import "@/less/memo-editor.less";
|
|
||||||
import ResourceListView from "./ResourceListView";
|
import ResourceListView from "./ResourceListView";
|
||||||
|
import RelationListView from "./RelationListView";
|
||||||
|
import "@/less/memo-editor.less";
|
||||||
|
|
||||||
const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "];
|
const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "];
|
||||||
const emptyOlReg = /^(\d+)\. $/;
|
const emptyOlReg = /^(\d+)\. $/;
|
||||||
@ -73,6 +74,7 @@ const MemoEditor = () => {
|
|||||||
handleEditorFocus();
|
handleEditorFocus();
|
||||||
editorStore.setMemoVisibility(memo.visibility);
|
editorStore.setMemoVisibility(memo.visibility);
|
||||||
editorStore.setResourceList(memo.resourceList);
|
editorStore.setResourceList(memo.resourceList);
|
||||||
|
editorStore.setRelationList(memo.relationList);
|
||||||
editorRef.current?.setContent(memo.content ?? "");
|
editorRef.current?.setContent(memo.content ?? "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -232,6 +234,7 @@ const MemoEditor = () => {
|
|||||||
content,
|
content,
|
||||||
visibility: editorState.memoVisibility,
|
visibility: editorState.memoVisibility,
|
||||||
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
||||||
|
relationList: editorState.relationList,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
editorStore.clearEditMemo();
|
editorStore.clearEditMemo();
|
||||||
@ -240,7 +243,7 @@ const MemoEditor = () => {
|
|||||||
content,
|
content,
|
||||||
visibility: editorState.memoVisibility,
|
visibility: editorState.memoVisibility,
|
||||||
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
||||||
relationList: [],
|
relationList: editorState.relationList,
|
||||||
});
|
});
|
||||||
filterStore.clearFilter();
|
filterStore.clearFilter();
|
||||||
}
|
}
|
||||||
@ -268,7 +271,8 @@ const MemoEditor = () => {
|
|||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
editorStore.clearResourceList();
|
editorStore.setResourceList([]);
|
||||||
|
editorStore.setRelationList([]);
|
||||||
setEditorContentCache("");
|
setEditorContentCache("");
|
||||||
editorRef.current?.setContent("");
|
editorRef.current?.setContent("");
|
||||||
clearContentQueryParam();
|
clearContentQueryParam();
|
||||||
@ -277,7 +281,8 @@ const MemoEditor = () => {
|
|||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
if (editorState.editMemoId) {
|
if (editorState.editMemoId) {
|
||||||
editorStore.clearEditMemo();
|
editorStore.clearEditMemo();
|
||||||
editorStore.clearResourceList();
|
editorStore.setResourceList([]);
|
||||||
|
editorStore.setRelationList([]);
|
||||||
editorRef.current?.setContent("");
|
editorRef.current?.setContent("");
|
||||||
setEditorContentCache("");
|
setEditorContentCache("");
|
||||||
}
|
}
|
||||||
@ -381,6 +386,7 @@ const MemoEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResourceListView />
|
<ResourceListView />
|
||||||
|
<RelationListView />
|
||||||
<div className="editor-footer-container">
|
<div className="editor-footer-container">
|
||||||
<MemoVisibilitySelector />
|
<MemoVisibilitySelector />
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
|
46
web/src/components/MemoRelationListView.tsx
Normal file
46
web/src/components/MemoRelationListView.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMemoCacheStore } from "@/store/zustand";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
relationList: MemoRelation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoRelationListView = (props: Props) => {
|
||||||
|
const memoCacheStore = useMemoCacheStore();
|
||||||
|
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
|
||||||
|
const relationList = props.relationList;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRelatedMemoList = async () => {
|
||||||
|
const requests = relationList.map((relation) => memoCacheStore.getOrFetchMemoById(relation.relatedMemoId));
|
||||||
|
const memoList = await Promise.all(requests);
|
||||||
|
setRelatedMemoList(memoList);
|
||||||
|
};
|
||||||
|
fetchRelatedMemoList();
|
||||||
|
}, [relationList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{relatedMemoList.length > 0 && (
|
||||||
|
<div className="w-full max-w-full overflow-hidden grid grid-cols-1 gap-1 mt-2">
|
||||||
|
{relatedMemoList.map((memo) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={memo.id}
|
||||||
|
className="w-auto flex flex-row justify-start items-center hover:bg-gray-100 dark:hover:bg-zinc-800 rounded text-sm p-1 text-gray-500 dark:text-gray-400 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="w-5 h-5 flex justify-center items-center shrink-0 bg-gray-100 dark:bg-zinc-800 rounded-full">
|
||||||
|
<Icon.Link className="w-3 h-auto" />
|
||||||
|
</div>
|
||||||
|
<span className="mx-1 w-auto truncate">{memo.content}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemoRelationListView;
|
@ -10,7 +10,7 @@ function Root() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-6xl mx-auto flex flex-row justify-center items-start">
|
<div className="w-full max-w-6xl mx-auto flex flex-row justify-center items-start">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="w-auto flex-grow flex flex-col justify-start items-start">
|
<main className="w-auto max-w-full flex-grow shrink flex flex-col justify-start items-start">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
@apply w-full mt-2 py-1 flex sm:flex-row flex-col justify-start items-start;
|
@apply w-full mt-2 py-1 flex sm:flex-row flex-col justify-start items-start;
|
||||||
|
|
||||||
> .normal-text {
|
> .normal-text {
|
||||||
@apply block flex-shrink-0 w-12 mr-3 sm:text-right text-left text-sm leading-8;
|
@apply block shrink-0 w-12 mr-3 sm:text-right text-left text-sm leading-8;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
.page-wrapper.memo-detail {
|
|
||||||
@apply relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800;
|
|
||||||
|
|
||||||
> .page-container {
|
|
||||||
@apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8;
|
|
||||||
|
|
||||||
> .page-header {
|
|
||||||
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800;
|
|
||||||
|
|
||||||
> .title-container {
|
|
||||||
@apply flex flex-row justify-start items-center;
|
|
||||||
|
|
||||||
> .logo-img {
|
|
||||||
@apply h-12 sm:h-14 w-auto mr-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .logo-text {
|
|
||||||
@apply text-4xl tracking-wide text-black dark:text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title-text {
|
|
||||||
@apply text-xl sm:text-3xl font-mono text-gray-700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .action-button-container {
|
|
||||||
> .btn {
|
|
||||||
@apply block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
@apply text-lg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .memos-wrapper {
|
|
||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4;
|
|
||||||
|
|
||||||
> .memo-container {
|
|
||||||
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700;
|
|
||||||
|
|
||||||
> .memo-header {
|
|
||||||
@apply mb-2 w-full flex flex-row justify-between items-center;
|
|
||||||
|
|
||||||
> .status-container {
|
|
||||||
@apply flex flex-row justify-start items-center text-sm text-gray-400;
|
|
||||||
|
|
||||||
> .name-text {
|
|
||||||
@apply ml-2 hover:text-green-600 hover:underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .memo-content {
|
|
||||||
@apply cursor-default;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
@apply cursor-default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -50,26 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .resource-list-wrapper {
|
|
||||||
@apply w-full flex flex-row justify-start flex-wrap;
|
|
||||||
|
|
||||||
> .resource-container {
|
|
||||||
@apply max-w-full mt-1 mr-1 flex flex-row justify-start items-center flex-nowrap bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply w-4 h-auto mr-1 text-gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name-text {
|
|
||||||
@apply text-gray-500 text-sm max-w-xs truncate font-mono;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .close-icon {
|
|
||||||
@apply w-4 h-auto ml-1 text-gray-500 hover:text-gray-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .editor-footer-container {
|
> .editor-footer-container {
|
||||||
@apply w-full flex flex-row justify-between items-center border-t border-t-gray-100 dark:border-t-zinc-500 py-3 mt-2;
|
@apply w-full flex flex-row justify-between items-center border-t border-t-gray-100 dark:border-t-zinc-500 py-3 mt-2;
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .btns-container {
|
> .btns-container {
|
||||||
@apply flex flex-row justify-end items-center relative flex-shrink-0;
|
@apply flex flex-row justify-end items-center relative shrink-0;
|
||||||
|
|
||||||
> .more-action-btns-wrapper {
|
> .more-action-btns-wrapper {
|
||||||
@apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3;
|
@apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3;
|
||||||
|
@ -140,7 +140,7 @@ const Auth = () => {
|
|||||||
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading && "opacity-80"}`}>
|
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading && "opacity-80"}`}>
|
||||||
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
|
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
|
||||||
<span
|
<span
|
||||||
className={`absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
|
className={`absolute top-3 left-3 px-1 leading-10 shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
|
||||||
username ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
|
username ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -156,7 +156,7 @@ const Auth = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
|
<div className="flex flex-col justify-start items-start relative w-full text-base mt-2 py-2">
|
||||||
<span
|
<span
|
||||||
className={`absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
|
className={`absolute top-3 left-3 px-1 leading-10 shrink-0 text-base cursor-text text-gray-400 transition-all select-none pointer-events-none ${
|
||||||
password ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
|
password ? "!text-sm !top-0 !z-10 !leading-4 bg-white dark:bg-zinc-800 rounded" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -32,7 +32,7 @@ function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-start items-start">
|
<div className="w-full flex flex-row justify-start items-start">
|
||||||
<div className="flex-grow w-auto max-w-2xl px-4 sm:px-2 sm:pt-4">
|
<div className="flex-grow shrink w-auto max-w-2xl px-4 sm:px-2 sm:pt-4">
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
|
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
|
||||||
{!userStore.isVisitorMode() && <MemoEditor />}
|
{!userStore.isVisitorMode() && <MemoEditor />}
|
||||||
|
@ -5,10 +5,7 @@ import { Link, useLocation, useParams } from "react-router-dom";
|
|||||||
import { UNKNOWN_ID } from "@/helpers/consts";
|
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||||
import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
|
import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import MemoContent from "@/components/MemoContent";
|
import Memo from "@/components/Memo";
|
||||||
import MemoResources from "@/components/MemoResources";
|
|
||||||
import { getDateTimeString } from "@/helpers/datetime";
|
|
||||||
import "@/less/memo-detail.less";
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
@ -49,23 +46,29 @@ const MemoDetail = () => {
|
|||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-wrapper memo-detail">
|
<section className="relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="page-container">
|
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8">
|
||||||
<div className="page-header">
|
<div className="sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="title-container">
|
<div className="flex flex-row justify-start items-center">
|
||||||
<img className="h-10 w-auto rounded-lg mr-2" src={customizedProfile.logoUrl} alt="" />
|
<img className="h-10 w-auto rounded-lg mr-2" src={customizedProfile.logoUrl} alt="" />
|
||||||
<p className="logo-text">{customizedProfile.name}</p>
|
<p className="text-4xl tracking-wide text-black dark:text-white">{customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-button-container">
|
<div className="action-button-container">
|
||||||
{!loadingState.isLoading && (
|
{!loadingState.isLoading && (
|
||||||
<>
|
<>
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link to="/" className="btn">
|
<Link
|
||||||
<span className="icon">🏠</span> {t("router.back-to-home")}
|
to="/"
|
||||||
|
className="block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline"
|
||||||
|
>
|
||||||
|
<span className="text-lg">🏠</span> {t("router.back-to-home")}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/auth" className="btn">
|
<Link
|
||||||
<span className="icon">👉</span> {t("common.sign-in")}
|
to="/auth"
|
||||||
|
className="block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline"
|
||||||
|
>
|
||||||
|
<span className="text-lg">👉</span> {t("common.sign-in")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -73,19 +76,8 @@ const MemoDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!loadingState.isLoading && (
|
{!loadingState.isLoading && (
|
||||||
<main className="memos-wrapper">
|
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
|
||||||
<div className="memo-container">
|
<Memo memo={state.memo} readonly showRelatedMemos />
|
||||||
<div className="memo-header">
|
|
||||||
<div className="status-container">
|
|
||||||
<span className="time-text">{getDateTimeString(state.memo.createdTs)}</span>
|
|
||||||
<a className="name-text" href={`/u/${state.memo.creatorId}`}>
|
|
||||||
@{state.memo.creatorName}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MemoContent className="memo-content" content={state.memo.content} showFull={true} onMemoContentClick={() => undefined} />
|
|
||||||
<MemoResources resourceList={state.memo.resourceList} />
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import store, { useAppSelector } from "..";
|
import store, { useAppSelector } from "..";
|
||||||
import { setEditMemoId, setMemoVisibility, setResourceList } from "../reducer/editor";
|
import { setEditMemoId, setMemoVisibility, setRelationList, setResourceList } from "../reducer/editor";
|
||||||
|
|
||||||
export const useEditorStore = () => {
|
export const useEditorStore = () => {
|
||||||
const state = useAppSelector((state) => state.editor);
|
const state = useAppSelector((state) => state.editor);
|
||||||
@ -21,8 +21,8 @@ export const useEditorStore = () => {
|
|||||||
setResourceList: (resourceList: Resource[]) => {
|
setResourceList: (resourceList: Resource[]) => {
|
||||||
store.dispatch(setResourceList(resourceList));
|
store.dispatch(setResourceList(resourceList));
|
||||||
},
|
},
|
||||||
clearResourceList: () => {
|
setRelationList: (relationList: MemoRelation[]) => {
|
||||||
store.dispatch(setResourceList([]));
|
store.dispatch(setRelationList(relationList));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,9 @@ import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
|||||||
import { useUserStore } from "./";
|
import { useUserStore } from "./";
|
||||||
import store, { useAppSelector } from "../";
|
import store, { useAppSelector } from "../";
|
||||||
import { createMemo, deleteMemo, patchMemo, setIsFetching, upsertMemos } from "../reducer/memo";
|
import { createMemo, deleteMemo, patchMemo, setIsFetching, upsertMemos } from "../reducer/memo";
|
||||||
|
import { useMemoCacheStore } from "../zustand/memo";
|
||||||
|
|
||||||
const convertResponseModelMemo = (memo: Memo): Memo => {
|
export const convertResponseModelMemo = (memo: Memo): Memo => {
|
||||||
return {
|
return {
|
||||||
...memo,
|
...memo,
|
||||||
createdTs: memo.createdTs * 1000,
|
createdTs: memo.createdTs * 1000,
|
||||||
@ -16,6 +17,7 @@ const convertResponseModelMemo = (memo: Memo): Memo => {
|
|||||||
export const useMemoStore = () => {
|
export const useMemoStore = () => {
|
||||||
const state = useAppSelector((state) => state.memo);
|
const state = useAppSelector((state) => state.memo);
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const memoCacheStore = useMemoCacheStore();
|
||||||
|
|
||||||
const fetchMemoById = async (memoId: MemoId) => {
|
const fetchMemoById = async (memoId: MemoId) => {
|
||||||
const { data } = (await api.getMemoById(memoId)).data;
|
const { data } = (await api.getMemoById(memoId)).data;
|
||||||
@ -44,6 +46,10 @@ export const useMemoStore = () => {
|
|||||||
store.dispatch(upsertMemos(fetchedMemos));
|
store.dispatch(upsertMemos(fetchedMemos));
|
||||||
store.dispatch(setIsFetching(false));
|
store.dispatch(setIsFetching(false));
|
||||||
|
|
||||||
|
for (const m of fetchedMemos) {
|
||||||
|
memoCacheStore.setMemoCache(m);
|
||||||
|
}
|
||||||
|
|
||||||
return fetchedMemos;
|
return fetchedMemos;
|
||||||
},
|
},
|
||||||
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
|
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => {
|
||||||
@ -54,8 +60,13 @@ export const useMemoStore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data } = (await api.getAllMemos(memoFind)).data;
|
const { data } = (await api.getAllMemos(memoFind)).data;
|
||||||
const memos = data.map((m) => convertResponseModelMemo(m));
|
const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
|
||||||
return memos;
|
|
||||||
|
for (const m of fetchedMemos) {
|
||||||
|
memoCacheStore.setMemoCache(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedMemos;
|
||||||
},
|
},
|
||||||
fetchArchivedMemos: async () => {
|
fetchArchivedMemos: async () => {
|
||||||
const memoFind: MemoFind = {
|
const memoFind: MemoFind = {
|
||||||
@ -88,12 +99,14 @@ export const useMemoStore = () => {
|
|||||||
const { data } = (await api.createMemo(memoCreate)).data;
|
const { data } = (await api.createMemo(memoCreate)).data;
|
||||||
const memo = convertResponseModelMemo(data);
|
const memo = convertResponseModelMemo(data);
|
||||||
store.dispatch(createMemo(memo));
|
store.dispatch(createMemo(memo));
|
||||||
|
memoCacheStore.setMemoCache(memo);
|
||||||
return memo;
|
return memo;
|
||||||
},
|
},
|
||||||
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
|
patchMemo: async (memoPatch: MemoPatch): Promise<Memo> => {
|
||||||
const { data } = (await api.patchMemo(memoPatch)).data;
|
const { data } = (await api.patchMemo(memoPatch)).data;
|
||||||
const memo = convertResponseModelMemo(data);
|
const memo = convertResponseModelMemo(data);
|
||||||
store.dispatch(patchMemo(omit(memo, "pinned")));
|
store.dispatch(patchMemo(omit(memo, "pinned")));
|
||||||
|
memoCacheStore.setMemoCache(memo);
|
||||||
return memo;
|
return memo;
|
||||||
},
|
},
|
||||||
pinMemo: async (memoId: MemoId) => {
|
pinMemo: async (memoId: MemoId) => {
|
||||||
@ -117,6 +130,7 @@ export const useMemoStore = () => {
|
|||||||
deleteMemoById: async (memoId: MemoId) => {
|
deleteMemoById: async (memoId: MemoId) => {
|
||||||
await api.deleteMemo(memoId);
|
await api.deleteMemo(memoId);
|
||||||
store.dispatch(deleteMemo(memoId));
|
store.dispatch(deleteMemo(memoId));
|
||||||
|
memoCacheStore.deleteMemoCache(memoId);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||||||
interface State {
|
interface State {
|
||||||
memoVisibility: Visibility;
|
memoVisibility: Visibility;
|
||||||
resourceList: Resource[];
|
resourceList: Resource[];
|
||||||
|
relationList: MemoRelation[];
|
||||||
editMemoId?: MemoId;
|
editMemoId?: MemoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ const editorSlice = createSlice({
|
|||||||
initialState: {
|
initialState: {
|
||||||
memoVisibility: "PRIVATE",
|
memoVisibility: "PRIVATE",
|
||||||
resourceList: [],
|
resourceList: [],
|
||||||
|
relationList: [],
|
||||||
} as State,
|
} as State,
|
||||||
reducers: {
|
reducers: {
|
||||||
setEditMemoId: (state, action: PayloadAction<Option<MemoId>>) => {
|
setEditMemoId: (state, action: PayloadAction<Option<MemoId>>) => {
|
||||||
@ -31,9 +33,15 @@ const editorSlice = createSlice({
|
|||||||
resourceList: action.payload,
|
resourceList: action.payload,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setRelationList: (state, action: PayloadAction<MemoRelation[]>) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
relationList: action.payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setEditMemoId, setMemoVisibility, setResourceList } = editorSlice.actions;
|
export const { setEditMemoId, setMemoVisibility, setResourceList, setRelationList } = editorSlice.actions;
|
||||||
|
|
||||||
export default editorSlice.reducer;
|
export default editorSlice.reducer;
|
||||||
|
2
web/src/store/zustand/index.ts
Normal file
2
web/src/store/zustand/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { useMemoCacheStore } from "./memo";
|
||||||
|
export { useMessageStore } from "./message";
|
41
web/src/store/zustand/memo.ts
Normal file
41
web/src/store/zustand/memo.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { combine } from "zustand/middleware";
|
||||||
|
import * as api from "@/helpers/api";
|
||||||
|
import { convertResponseModelMemo } from "../module";
|
||||||
|
|
||||||
|
export const useMemoCacheStore = create(
|
||||||
|
combine({ memoById: new Map<MemoId, Memo>() }, (set, get) => ({
|
||||||
|
getState: () => get(),
|
||||||
|
getOrFetchMemoById: async (memoId: MemoId) => {
|
||||||
|
const memo = get().memoById.get(memoId);
|
||||||
|
if (memo) {
|
||||||
|
return memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = (await api.getMemoById(memoId)).data;
|
||||||
|
const formatedMemo = convertResponseModelMemo(data);
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
state.memoById.set(memoId, formatedMemo);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatedMemo;
|
||||||
|
},
|
||||||
|
getMemoById: (memoId: MemoId) => {
|
||||||
|
return get().memoById.get(memoId);
|
||||||
|
},
|
||||||
|
setMemoCache: (memo: Memo) => {
|
||||||
|
set((state) => {
|
||||||
|
state.memoById.set(memo.id, memo);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteMemoCache: (memoId: MemoId) => {
|
||||||
|
set((state) => {
|
||||||
|
state.memoById.delete(memoId);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user