Commit 68c2bd38 authored by Steven's avatar Steven

chore: update memo relations

parent 82da20e1
...@@ -2,16 +2,18 @@ syntax = "proto3"; ...@@ -2,16 +2,18 @@ syntax = "proto3";
package memos.api.v1; package memos.api.v1;
import "google/api/field_behavior.proto";
option go_package = "gen/api/v1"; option go_package = "gen/api/v1";
message MemoRelation { message MemoRelation {
// The name of memo. // The name of memo.
// Format: "memos/{uid}" // Format: "memos/{uid}"
string memo = 1; Memo memo = 1;
// The name of related memo. // The name of related memo.
// Format: "memos/{uid}" // Format: "memos/{uid}"
string related_memo = 2; Memo related_memo = 2;
enum Type { enum Type {
TYPE_UNSPECIFIED = 0; TYPE_UNSPECIFIED = 0;
...@@ -19,4 +21,13 @@ message MemoRelation { ...@@ -19,4 +21,13 @@ message MemoRelation {
COMMENT = 2; COMMENT = 2;
} }
Type type = 3; Type type = 3;
message Memo {
// The name of the memo.
// Format: memos/{id}
string name = 1;
string uid = 2;
// The snippet of the memo content. Plain text only.
string snippet = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
}
} }
...@@ -172,7 +172,7 @@ message Memo { ...@@ -172,7 +172,7 @@ message Memo {
optional string parent = 18 [(google.api.field_behavior) = OUTPUT_ONLY]; optional string parent = 18 [(google.api.field_behavior) = OUTPUT_ONLY];
// The snippet of the memo content. Plain text only. // The snippet of the memo content. Plain text only.
string snippet = 19; string snippet = 19 [(google.api.field_behavior) = OUTPUT_ONLY];
// The location of the memo. // The location of the memo.
optional Location location = 20; optional Location location = 20;
......
This diff is collapsed.
...@@ -347,7 +347,7 @@ paths: ...@@ -347,7 +347,7 @@ paths:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -368,7 +368,7 @@ paths: ...@@ -368,7 +368,7 @@ paths:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -813,7 +813,7 @@ paths: ...@@ -813,7 +813,7 @@ paths:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -897,6 +897,7 @@ paths: ...@@ -897,6 +897,7 @@ paths:
snippet: snippet:
type: string type: string
description: The snippet of the memo content. Plain text only. description: The snippet of the memo content. Plain text only.
readOnly: true
location: location:
$ref: '#/definitions/apiv1Location' $ref: '#/definitions/apiv1Location'
description: The location of the memo. description: The location of the memo.
...@@ -1056,7 +1057,7 @@ paths: ...@@ -1056,7 +1057,7 @@ paths:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -1257,7 +1258,7 @@ paths: ...@@ -1257,7 +1258,7 @@ paths:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -1952,6 +1953,82 @@ definitions: ...@@ -1952,6 +1953,82 @@ definitions:
longitude: longitude:
type: number type: number
format: double format: double
apiv1Memo:
type: object
properties:
name:
type: string
description: |-
The name of the memo.
Format: memos/{id}
id is the system generated id.
uid:
type: string
description: The user defined id of the memo.
rowStatus:
$ref: '#/definitions/v1RowStatus'
creator:
type: string
title: |-
The name of the creator.
Format: users/{id}
createTime:
type: string
format: date-time
updateTime:
type: string
format: date-time
displayTime:
type: string
format: date-time
content:
type: string
nodes:
type: array
items:
type: object
$ref: '#/definitions/v1Node'
readOnly: true
visibility:
$ref: '#/definitions/v1Visibility'
tags:
type: array
items:
type: string
pinned:
type: boolean
resources:
type: array
items:
type: object
$ref: '#/definitions/v1Resource'
relations:
type: array
items:
type: object
$ref: '#/definitions/v1MemoRelation'
reactions:
type: array
items:
type: object
$ref: '#/definitions/v1Reaction'
readOnly: true
property:
$ref: '#/definitions/v1MemoProperty'
readOnly: true
parent:
type: string
title: |-
The name of the parent memo.
Format: memos/{id}
readOnly: true
snippet:
type: string
description: The snippet of the memo content. Plain text only.
readOnly: true
location:
$ref: '#/definitions/apiv1Location'
description: The location of the memo.
apiv1OAuth2Config: apiv1OAuth2Config:
type: object type: object
properties: properties:
...@@ -2482,7 +2559,7 @@ definitions: ...@@ -2482,7 +2559,7 @@ definitions:
type: array type: array
items: items:
type: object type: object
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
v1ListMemoReactionsResponse: v1ListMemoReactionsResponse:
type: object type: object
properties: properties:
...@@ -2514,7 +2591,7 @@ definitions: ...@@ -2514,7 +2591,7 @@ definitions:
type: array type: array
items: items:
type: object type: object
$ref: '#/definitions/v1Memo' $ref: '#/definitions/apiv1Memo'
nextPageToken: nextPageToken:
type: string type: string
description: |- description: |-
...@@ -2575,81 +2652,6 @@ definitions: ...@@ -2575,81 +2652,6 @@ definitions:
properties: properties:
content: content:
type: string type: string
v1Memo:
type: object
properties:
name:
type: string
description: |-
The name of the memo.
Format: memos/{id}
id is the system generated id.
uid:
type: string
description: The user defined id of the memo.
rowStatus:
$ref: '#/definitions/v1RowStatus'
creator:
type: string
title: |-
The name of the creator.
Format: users/{id}
createTime:
type: string
format: date-time
updateTime:
type: string
format: date-time
displayTime:
type: string
format: date-time
content:
type: string
nodes:
type: array
items:
type: object
$ref: '#/definitions/v1Node'
readOnly: true
visibility:
$ref: '#/definitions/v1Visibility'
tags:
type: array
items:
type: string
pinned:
type: boolean
resources:
type: array
items:
type: object
$ref: '#/definitions/v1Resource'
relations:
type: array
items:
type: object
$ref: '#/definitions/v1MemoRelation'
reactions:
type: array
items:
type: object
$ref: '#/definitions/v1Reaction'
readOnly: true
property:
$ref: '#/definitions/v1MemoProperty'
readOnly: true
parent:
type: string
title: |-
The name of the parent memo.
Format: memos/{id}
readOnly: true
snippet:
type: string
description: The snippet of the memo content. Plain text only.
location:
$ref: '#/definitions/apiv1Location'
description: The location of the memo.
v1MemoProperty: v1MemoProperty:
type: object type: object
properties: properties:
...@@ -2669,17 +2671,31 @@ definitions: ...@@ -2669,17 +2671,31 @@ definitions:
type: object type: object
properties: properties:
memo: memo:
type: string $ref: '#/definitions/v1MemoRelationMemo'
title: |- title: |-
The name of memo. The name of memo.
Format: "memos/{uid}" Format: "memos/{uid}"
relatedMemo: relatedMemo:
type: string $ref: '#/definitions/v1MemoRelationMemo'
title: |- title: |-
The name of related memo. The name of related memo.
Format: "memos/{uid}" Format: "memos/{uid}"
type: type:
$ref: '#/definitions/v1MemoRelationType' $ref: '#/definitions/v1MemoRelationType'
v1MemoRelationMemo:
type: object
properties:
name:
type: string
title: |-
The name of the memo.
Format: memos/{id}
uid:
type: string
snippet:
type: string
description: The snippet of the memo content. Plain text only.
readOnly: true
v1MemoRelationType: v1MemoRelationType:
type: string type: string
enum: enum:
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
"github.com/pkg/errors"
v1pb "github.com/usememos/memos/proto/gen/api/v1" v1pb "github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
...@@ -28,7 +29,7 @@ func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMe ...@@ -28,7 +29,7 @@ func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMe
for _, relation := range request.Relations { for _, relation := range request.Relations {
// Ignore reflexive relations. // Ignore reflexive relations.
if request.Name == relation.RelatedMemo { if request.Name == relation.RelatedMemo.Name {
continue continue
} }
// Ignore comment relations as there's no need to update a comment's relation. // Ignore comment relations as there's no need to update a comment's relation.
...@@ -36,7 +37,7 @@ func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMe ...@@ -36,7 +37,7 @@ func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMe
if relation.Type == v1pb.MemoRelation_COMMENT { if relation.Type == v1pb.MemoRelation_COMMENT {
continue continue
} }
relatedMemoID, err := ExtractMemoIDFromName(relation.RelatedMemo) relatedMemoID, err := ExtractMemoIDFromName(relation.RelatedMemo.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid related memo name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid related memo name: %v", err)
} }
...@@ -64,8 +65,12 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List ...@@ -64,8 +65,12 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, relation := range tempList { for _, raw := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation)) relation, err := s.convertMemoRelationFromStore(ctx, raw)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert memo relation")
}
relationList = append(relationList, relation)
} }
tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
RelatedMemoID: &id, RelatedMemoID: &id,
...@@ -73,8 +78,12 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List ...@@ -73,8 +78,12 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, relation := range tempList { for _, raw := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation)) relation, err := s.convertMemoRelationFromStore(ctx, raw)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert memo relation")
}
relationList = append(relationList, relation)
} }
response := &v1pb.ListMemoRelationsResponse{ response := &v1pb.ListMemoRelationsResponse{
...@@ -83,12 +92,36 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List ...@@ -83,12 +92,36 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List
return response, nil return response, nil
} }
func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *v1pb.MemoRelation { func (s *APIV1Service) convertMemoRelationFromStore(ctx context.Context, memoRelation *store.MemoRelation) (*v1pb.MemoRelation, error) {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.MemoID})
if err != nil {
return nil, err
}
memoSnippet, err := getMemoContentSnippet(memo.Content)
if err != nil {
return nil, errors.Wrap(err, "failed to get memo content snippet")
}
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.RelatedMemoID})
if err != nil {
return nil, err
}
relatedMemoSnippet, err := getMemoContentSnippet(relatedMemo.Content)
if err != nil {
return nil, errors.Wrap(err, "failed to get related memo content snippet")
}
return &v1pb.MemoRelation{ return &v1pb.MemoRelation{
Memo: fmt.Sprintf("%s%d", MemoNamePrefix, memoRelation.MemoID), Memo: &v1pb.MemoRelation_Memo{
RelatedMemo: fmt.Sprintf("%s%d", MemoNamePrefix, memoRelation.RelatedMemoID), Name: fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID),
Uid: memo.UID,
Snippet: memoSnippet,
},
RelatedMemo: &v1pb.MemoRelation_Memo{
Name: fmt.Sprintf("%s%d", MemoNamePrefix, relatedMemo.ID),
Uid: relatedMemo.UID,
Snippet: relatedMemoSnippet,
},
Type: convertMemoRelationTypeFromStore(memoRelation.Type), Type: convertMemoRelationTypeFromStore(memoRelation.Type),
} }, nil
} }
func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) v1pb.MemoRelation_Type { func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) v1pb.MemoRelation_Type {
......
...@@ -73,7 +73,7 @@ const EmbeddedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { ...@@ -73,7 +73,7 @@ const EmbeddedMemo = ({ resourceId: uid, params: paramsStr }: Props) => {
</div> </div>
<div className="flex justify-end items-center gap-1"> <div className="flex justify-end items-center gap-1">
<span className="text-xs opacity-60 leading-5 cursor-pointer hover:opacity-80" onClick={() => copyMemoUid(memo.uid)}> <span className="text-xs opacity-60 leading-5 cursor-pointer hover:opacity-80" onClick={() => copyMemoUid(memo.uid)}>
{memo.uid.slice(0, 8)} {memo.uid.slice(0, 6)}
</span> </span>
<Link className="opacity-60 hover:opacity-80" to={`/m/${memo.uid}`} unstable_viewTransition> <Link className="opacity-60 hover:opacity-80" to={`/m/${memo.uid}`} unstable_viewTransition>
<ArrowUpRightIcon className="w-5 h-auto" /> <ArrowUpRightIcon className="w-5 h-auto" />
......
...@@ -8,7 +8,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover ...@@ -8,7 +8,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover
import { memoServiceClient } from "@/grpcweb"; import { memoServiceClient } from "@/grpcweb";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; import { MemoRelation_Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
import { Memo, MemoView } from "@/types/proto/api/v1/memo_service"; import { Memo, MemoView } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { EditorRefActions } from "../Editor"; import { EditorRefActions } from "../Editor";
...@@ -34,7 +34,7 @@ const AddMemoRelationPopover = (props: Props) => { ...@@ -34,7 +34,7 @@ const AddMemoRelationPopover = (props: Props) => {
(memo) => (memo) =>
!selectedMemos.includes(memo) && !selectedMemos.includes(memo) &&
memo.name !== context.memoName && memo.name !== context.memoName &&
!context.relationList.some((relation) => relation.relatedMemo === memo.name), !context.relationList.some((relation) => relation.relatedMemo?.name === memo.name),
); );
useDebounce( useDebounce(
...@@ -112,8 +112,8 @@ const AddMemoRelationPopover = (props: Props) => { ...@@ -112,8 +112,8 @@ const AddMemoRelationPopover = (props: Props) => {
uniqBy( uniqBy(
[ [
...selectedMemos.map((memo) => ({ ...selectedMemos.map((memo) => ({
memo: context.memoName || "", memo: MemoRelation_Memo.fromPartial({ name: memo.name }),
relatedMemo: memo.name, relatedMemo: MemoRelation_Memo.fromPartial({ name: memo.name }),
type: MemoRelation_Type.REFERENCE, type: MemoRelation_Type.REFERENCE,
})), })),
...context.relationList, ...context.relationList,
......
...@@ -19,7 +19,7 @@ const RelationListView = (props: Props) => { ...@@ -19,7 +19,7 @@ const RelationListView = (props: Props) => {
const requests = relationList const requests = relationList
.filter((relation) => relation.type === MemoRelation_Type.REFERENCE) .filter((relation) => relation.type === MemoRelation_Type.REFERENCE)
.map(async (relation) => { .map(async (relation) => {
return await memoStore.getOrFetchMemoByName(relation.relatedMemo, { skipStore: true }); return await memoStore.getOrFetchMemoByName(relation.relatedMemo!.name, { skipStore: true });
}); });
const list = await Promise.all(requests); const list = await Promise.all(requests);
setReferencingMemoList(list); setReferencingMemoList(list);
...@@ -27,7 +27,7 @@ const RelationListView = (props: Props) => { ...@@ -27,7 +27,7 @@ const RelationListView = (props: Props) => {
}, [relationList]); }, [relationList]);
const handleDeleteRelation = async (memo: Memo) => { const handleDeleteRelation = async (memo: Memo) => {
setRelationList(relationList.filter((relation) => relation.relatedMemo !== memo.name)); setRelationList(relationList.filter((relation) => relation.relatedMemo?.name !== memo.name));
}; };
return ( return (
......
...@@ -80,7 +80,8 @@ const MemoEditor = (props: Props) => { ...@@ -80,7 +80,8 @@ const MemoEditor = (props: Props) => {
const [contentCache, setContentCache] = useLocalStorage<string>(contentCacheKey, ""); const [contentCache, setContentCache] = useLocalStorage<string>(contentCacheKey, "");
const referenceRelations = memoName const referenceRelations = memoName
? state.relationList.filter( ? state.relationList.filter(
(relation) => relation.memo === memoName && relation.relatedMemo !== memoName && relation.type === MemoRelation_Type.REFERENCE, (relation) =>
relation.memo?.name === memoName && relation.relatedMemo?.name !== memoName && relation.type === MemoRelation_Type.REFERENCE,
) )
: state.relationList.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); : state.relationList.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
const workspaceMemoRelatedSetting = const workspaceMemoRelatedSetting =
......
import { useColorScheme } from "@mui/joy"; import { useColorScheme } from "@mui/joy";
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import ForceGraph2D from "react-force-graph-2d"; import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
import useNavigateTo from "@/hooks/useNavigateTo";
import { Memo } from "@/types/proto/api/v1/memo_service"; import { Memo } from "@/types/proto/api/v1/memo_service";
import { FGMethods } from "./types"; import { LinkType, NodeType } from "./types";
import { convertMemoRelationsToGraphData } from "./utils"; import { convertMemoRelationsToGraphData } from "./utils";
interface Props { interface Props {
...@@ -15,9 +16,10 @@ const MAIN_NODE_COLOR = "#14b8a6"; ...@@ -15,9 +16,10 @@ const MAIN_NODE_COLOR = "#14b8a6";
const DEFAULT_NODE_COLOR = "#a1a1aa"; const DEFAULT_NODE_COLOR = "#a1a1aa";
const MemoRelationForceGraph = ({ className, memo }: Props) => { const MemoRelationForceGraph = ({ className, memo }: Props) => {
const navigateTo = useNavigateTo();
const { mode } = useColorScheme(); const { mode } = useColorScheme();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<FGMethods | undefined>(undefined); const graphRef = useRef<ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> | undefined>(undefined);
const [graphSize, setGraphSize] = useState({ width: 0, height: 0 }); const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });
useEffect(() => { useEffect(() => {
...@@ -25,8 +27,9 @@ const MemoRelationForceGraph = ({ className, memo }: Props) => { ...@@ -25,8 +27,9 @@ const MemoRelationForceGraph = ({ className, memo }: Props) => {
setGraphSize(containerRef.current.getBoundingClientRect()); setGraphSize(containerRef.current.getBoundingClientRect());
}, []); }, []);
const onNodeClick = () => { const onNodeClick = (node: NodeObject<NodeType>) => {
// TODO: Handle node click event if (node.memo.uid === memo.uid) return;
navigateTo(`/m/${node.memo.uid}`);
}; };
return ( return (
...@@ -37,11 +40,15 @@ const MemoRelationForceGraph = ({ className, memo }: Props) => { ...@@ -37,11 +40,15 @@ const MemoRelationForceGraph = ({ className, memo }: Props) => {
height={graphSize.height} height={graphSize.height}
enableZoomInteraction enableZoomInteraction
cooldownTicks={0} cooldownTicks={0}
nodeColor={(node) => (node.name === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)} nodeColor={(node) => (node.id === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)}
nodeRelSize={3} nodeRelSize={3}
linkColor={() => (mode === "light" ? "" : "#525252")} nodeLabel={(node) => node.memo.uid.slice(0, 6).toLowerCase()}
linkColor={() => (mode === "light" ? "#e4e4e7" : "#3f3f46")}
graphData={convertMemoRelationsToGraphData(memo.relations)} graphData={convertMemoRelationsToGraphData(memo.relations)}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
linkDirectionalArrowLength={3}
linkDirectionalArrowRelPos={1}
linkCurvature={0.25}
/> />
</div> </div>
); );
......
import { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d"; import { MemoRelation_Memo } from "@/types/proto/api/v1/memo_relation_service";
export interface NodeType { export interface NodeType {
name: string; memo: MemoRelation_Memo;
} }
export interface LinkType { export interface LinkType {
// ...add more additional properties relevant to the link here. // ...add more additional properties relevant to the link here.
} }
export interface FGMethods extends ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> {}
import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d"; import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
import { MemoRelation } from "@/types/proto/api/v1/memo_relation_service"; import { MemoRelation, MemoRelation_Memo } from "@/types/proto/api/v1/memo_relation_service";
import { LinkType, NodeType } from "./types"; import { LinkType, NodeType } from "./types";
export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => { export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => {
...@@ -8,23 +8,24 @@ export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): ...@@ -8,23 +8,24 @@ export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]):
// Iterate through memoRelations to populate nodes and links. // Iterate through memoRelations to populate nodes and links.
memoRelations.forEach((relation) => { memoRelations.forEach((relation) => {
const { memo, relatedMemo, type } = relation; const memo = relation.memo as MemoRelation_Memo;
const relatedMemo = relation.relatedMemo as MemoRelation_Memo;
// Add memo node if not already present. // Add memo node if not already present.
if (!nodesMap.has(memo)) { if (!nodesMap.has(memo.name)) {
nodesMap.set(memo, { id: memo, name: memo }); nodesMap.set(memo.name, { id: memo.name, memo });
} }
// Add related_memo node if not already present. // Add related_memo node if not already present.
if (!nodesMap.has(relatedMemo)) { if (!nodesMap.has(relatedMemo.name)) {
nodesMap.set(relatedMemo, { id: relatedMemo, name: relatedMemo }); nodesMap.set(relatedMemo.name, { id: relatedMemo.name, memo: relatedMemo });
} }
// Create link between memo and relatedMemo. // Create link between memo and relatedMemo.
links.push({ links.push({
source: memo, source: memo.name,
target: relatedMemo, target: relatedMemo.name,
type, // Include the type of relation as a property of the link. type: relation.type, // Include the type of relation as a property of the link.
}); });
}); });
......
import clsx from "clsx"; import clsx from "clsx";
import { DotIcon, LinkIcon, MilestoneIcon } from "lucide-react"; import { LinkIcon, MilestoneIcon } from "lucide-react";
import { memo, useState } from "react"; import { memo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import { useMemoStore } from "@/store/v1";
import { MemoRelation } from "@/types/proto/api/v1/memo_relation_service"; import { MemoRelation } from "@/types/proto/api/v1/memo_relation_service";
import { Memo } from "@/types/proto/api/v1/memo_service"; import { Memo } from "@/types/proto/api/v1/memo_service";
...@@ -14,30 +12,15 @@ interface Props { ...@@ -14,30 +12,15 @@ interface Props {
const MemoRelationListView = (props: Props) => { const MemoRelationListView = (props: Props) => {
const { memo, relations: relationList } = props; const { memo, relations: relationList } = props;
const memoStore = useMemoStore(); const referencingMemoList = relationList
const [referencingMemoList, setReferencingMemoList] = useState<Memo[]>([]); .filter((relation) => relation.memo?.name === memo.name && relation.relatedMemo?.name !== memo.name)
const [referencedMemoList, setReferencedMemoList] = useState<Memo[]>([]); .map((relation) => relation.relatedMemo!);
const [selectedTab, setSelectedTab] = useState<"referencing" | "referenced">("referencing"); const referencedMemoList = relationList
.filter((relation) => relation.memo?.name !== memo.name && relation.relatedMemo?.name === memo.name)
useAsyncEffect(async () => { .map((relation) => relation.memo!);
const referencingMemoList = await Promise.all( const [selectedTab, setSelectedTab] = useState<"referencing" | "referenced">(
relationList referencingMemoList.length === 0 ? "referenced" : "referencing",
.filter((relation) => relation.memo === memo.name && relation.relatedMemo !== memo.name)
.map((relation) => memoStore.getOrFetchMemoByName(relation.relatedMemo, { skipStore: true })),
);
setReferencingMemoList(referencingMemoList);
const referencedMemoList = await Promise.all(
relationList
.filter((relation) => relation.memo !== memo.name && relation.relatedMemo === memo.name)
.map((relation) => memoStore.getOrFetchMemoByName(relation.memo, { skipStore: true })),
); );
setReferencedMemoList(referencedMemoList);
if (referencingMemoList.length === 0) {
setSelectedTab("referenced");
} else {
setSelectedTab("referencing");
}
}, [memo.name, relationList]);
if (referencingMemoList.length + referencedMemoList.length === 0) { if (referencingMemoList.length + referencedMemoList.length === 0) {
return null; return null;
...@@ -83,7 +66,9 @@ const MemoRelationListView = (props: Props) => { ...@@ -83,7 +66,9 @@ const MemoRelationListView = (props: Props) => {
to={`/m/${memo.uid}`} to={`/m/${memo.uid}`}
unstable_viewTransition unstable_viewTransition
> >
<DotIcon className="shrink-0 w-4 h-auto opacity-40" /> <span className="text-xs opacity-60 leading-4 border font-mono px-1 rounded-full mr-1 dark:border-zinc-700">
{memo.uid.slice(0, 6)}
</span>
<span className="truncate">{memo.snippet}</span> <span className="truncate">{memo.snippet}</span>
</Link> </Link>
); );
...@@ -100,7 +85,9 @@ const MemoRelationListView = (props: Props) => { ...@@ -100,7 +85,9 @@ const MemoRelationListView = (props: Props) => {
to={`/m/${memo.uid}`} to={`/m/${memo.uid}`}
unstable_viewTransition unstable_viewTransition
> >
<DotIcon className="shrink-0 w-4 h-auto opacity-40" /> <span className="text-xs opacity-60 leading-4 border font-mono px-1 rounded-full mr-1 dark:border-zinc-700">
{memo.uid.slice(0, 6)}
</span>
<span className="truncate">{memo.snippet}</span> <span className="truncate">{memo.snippet}</span>
</Link> </Link>
); );
......
...@@ -52,7 +52,7 @@ const MemoView: React.FC<Props> = (props: Props) => { ...@@ -52,7 +52,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
const memoContainerRef = useRef<HTMLDivElement>(null); const memoContainerRef = useRef<HTMLDivElement>(null);
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
const commentAmount = memo.relations.filter( const commentAmount = memo.relations.filter(
(relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemo === memo.name, (relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemo?.name === memo.name,
).length; ).length;
const relativeTimeFormat = Date.now() - memo.displayTime!.getTime() > 1000 * 60 * 60 * 24 ? "datetime" : "auto"; const relativeTimeFormat = Date.now() - memo.displayTime!.getTime() > 1000 * 60 * 60 * 24 ? "datetime" : "auto";
const readonly = memo.creator !== user?.name && !isSuperUser(user); const readonly = memo.creator !== user?.name && !isSuperUser(user);
......
...@@ -34,8 +34,8 @@ const MemoDetail = () => { ...@@ -34,8 +34,8 @@ const MemoDetail = () => {
const [parentMemo, setParentMemo] = useState<Memo | undefined>(undefined); const [parentMemo, setParentMemo] = useState<Memo | undefined>(undefined);
const [showCommentEditor, setShowCommentEditor] = useState(false); const [showCommentEditor, setShowCommentEditor] = useState(false);
const commentRelations = const commentRelations =
memo?.relations.filter((relation) => relation.relatedMemo === memo.name && relation.type === MemoRelation_Type.COMMENT) || []; memo?.relations.filter((relation) => relation.relatedMemo?.name === memo.name && relation.type === MemoRelation_Type.COMMENT) || [];
const comments = commentRelations.map((relation) => memoStore.getMemoByName(relation.memo)).filter((memo) => memo) as any as Memo[]; const comments = commentRelations.map((relation) => memoStore.getMemoByName(relation.memo!.name)).filter((memo) => memo) as any as Memo[];
const showCreateCommentButton = workspaceMemoRelatedSetting.enableComment && currentUser; const showCreateCommentButton = workspaceMemoRelatedSetting.enableComment && currentUser;
// Prepare memo. // Prepare memo.
...@@ -64,7 +64,7 @@ const MemoDetail = () => { ...@@ -64,7 +64,7 @@ const MemoDetail = () => {
} else { } else {
setParentMemo(undefined); setParentMemo(undefined);
} }
await Promise.all(commentRelations.map((relation) => memoStore.getOrFetchMemoByName(relation.memo))); await Promise.all(commentRelations.map((relation) => memoStore.getOrFetchMemoByName(relation.memo!.name)));
})(); })();
}, [memo]); }, [memo]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment