refactor: consolidate MemoEditor components (#5409)
This commit is contained in:
parent
a630b70ba9
commit
a6e8ba7fb2
8 changed files with 211 additions and 247 deletions
|
|
@ -1,39 +1,32 @@
|
||||||
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, XIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, PaperclipIcon, XIcon } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import type { AttachmentItem } from "@/components/memo-metadata/types";
|
import type { LocalFile } from "@/components/memo-metadata/types";
|
||||||
|
import { toAttachmentItems } from "@/components/memo-metadata/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||||
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
|
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
|
||||||
|
|
||||||
interface AttachmentItemCardProps {
|
interface AttachmentListProps {
|
||||||
item: AttachmentItem;
|
attachments: Attachment[];
|
||||||
|
localFiles?: LocalFile[];
|
||||||
|
onAttachmentsChange?: (attachments: Attachment[]) => void;
|
||||||
|
onRemoveLocalFile?: (previewUrl: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttachmentItemCard: FC<{
|
||||||
|
item: ReturnType<typeof toAttachmentItems>[0];
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onMoveUp?: () => void;
|
onMoveUp?: () => void;
|
||||||
onMoveDown?: () => void;
|
onMoveDown?: () => void;
|
||||||
canMoveUp?: boolean;
|
canMoveUp?: boolean;
|
||||||
canMoveDown?: boolean;
|
canMoveDown?: boolean;
|
||||||
className?: string;
|
}> = ({ item, onRemove, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true }) => {
|
||||||
}
|
|
||||||
|
|
||||||
const AttachmentItemCard: FC<AttachmentItemCardProps> = ({
|
|
||||||
item,
|
|
||||||
onRemove,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
canMoveUp = true,
|
|
||||||
canMoveDown = true,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const { category, filename, thumbnailUrl, mimeType, size, isLocal } = item;
|
const { category, filename, thumbnailUrl, mimeType, size, isLocal } = item;
|
||||||
const fileTypeLabel = getFileTypeLabel(mimeType);
|
const fileTypeLabel = getFileTypeLabel(mimeType);
|
||||||
const fileSizeLabel = size ? formatFileSize(size) : undefined;
|
const fileSizeLabel = size ? formatFileSize(size) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all">
|
||||||
className={cn(
|
|
||||||
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 w-6 h-6 rounded overflow-hidden bg-muted/40 flex items-center justify-center">
|
<div className="flex-shrink-0 w-6 h-6 rounded overflow-hidden bg-muted/40 flex items-center justify-center">
|
||||||
{category === "image" && thumbnailUrl ? (
|
{category === "image" && thumbnailUrl ? (
|
||||||
<img src={thumbnailUrl} alt="" className="w-full h-full object-cover" />
|
<img src={thumbnailUrl} alt="" className="w-full h-full object-cover" />
|
||||||
|
|
@ -113,4 +106,70 @@ const AttachmentItemCard: FC<AttachmentItemCardProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AttachmentItemCard;
|
const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
|
||||||
|
if (attachments.length === 0 && localFiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = toAttachmentItems(attachments, localFiles);
|
||||||
|
|
||||||
|
const handleMoveUp = (index: number) => {
|
||||||
|
if (index === 0 || !onAttachmentsChange) return;
|
||||||
|
|
||||||
|
const newAttachments = [...attachments];
|
||||||
|
[newAttachments[index - 1], newAttachments[index]] = [newAttachments[index], newAttachments[index - 1]];
|
||||||
|
onAttachmentsChange(newAttachments);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDown = (index: number) => {
|
||||||
|
if (index === attachments.length - 1 || !onAttachmentsChange) return;
|
||||||
|
|
||||||
|
const newAttachments = [...attachments];
|
||||||
|
[newAttachments[index], newAttachments[index + 1]] = [newAttachments[index + 1], newAttachments[index]];
|
||||||
|
onAttachmentsChange(newAttachments);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveAttachment = (name: string) => {
|
||||||
|
if (onAttachmentsChange) {
|
||||||
|
onAttachmentsChange(attachments.filter((attachment) => attachment.name !== name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveItem = (item: (typeof items)[0]) => {
|
||||||
|
if (item.isLocal) {
|
||||||
|
onRemoveLocalFile?.(item.id);
|
||||||
|
} else {
|
||||||
|
handleRemoveAttachment(item.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-muted/30">
|
||||||
|
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Attachments ({items.length})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||||
|
{items.map((item) => {
|
||||||
|
const isLocalFile = item.isLocal;
|
||||||
|
const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AttachmentItemCard
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onRemove={() => handleRemoveItem(item)}
|
||||||
|
onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}
|
||||||
|
onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}
|
||||||
|
canMoveUp={!isLocalFile && attachmentIndex > 0}
|
||||||
|
canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttachmentList;
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import { PaperclipIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import type { LocalFile } from "@/components/memo-metadata/types";
|
|
||||||
import { toAttachmentItems } from "@/components/memo-metadata/types";
|
|
||||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
|
||||||
import AttachmentItemCard from "./AttachmentItemCard";
|
|
||||||
|
|
||||||
interface AttachmentListV2Props {
|
|
||||||
attachments: Attachment[];
|
|
||||||
localFiles?: LocalFile[];
|
|
||||||
onAttachmentsChange?: (attachments: Attachment[]) => void;
|
|
||||||
onRemoveLocalFile?: (previewUrl: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AttachmentListV2: FC<AttachmentListV2Props> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
|
|
||||||
if (attachments.length === 0 && localFiles.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = toAttachmentItems(attachments, localFiles);
|
|
||||||
|
|
||||||
const handleMoveUp = (index: number) => {
|
|
||||||
if (index === 0 || !onAttachmentsChange) return;
|
|
||||||
|
|
||||||
const newAttachments = [...attachments];
|
|
||||||
[newAttachments[index - 1], newAttachments[index]] = [newAttachments[index], newAttachments[index - 1]];
|
|
||||||
onAttachmentsChange(newAttachments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMoveDown = (index: number) => {
|
|
||||||
if (index === attachments.length - 1 || !onAttachmentsChange) return;
|
|
||||||
|
|
||||||
const newAttachments = [...attachments];
|
|
||||||
[newAttachments[index], newAttachments[index + 1]] = [newAttachments[index + 1], newAttachments[index]];
|
|
||||||
onAttachmentsChange(newAttachments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveAttachment = (name: string) => {
|
|
||||||
if (onAttachmentsChange) {
|
|
||||||
onAttachmentsChange(attachments.filter((attachment) => attachment.name !== name));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveItem = (item: (typeof items)[0]) => {
|
|
||||||
if (item.isLocal) {
|
|
||||||
onRemoveLocalFile?.(item.id);
|
|
||||||
} else {
|
|
||||||
handleRemoveAttachment(item.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-muted/30">
|
|
||||||
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
|
||||||
<span className="text-xs font-medium text-muted-foreground">Attachments ({items.length})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
|
||||||
{items.map((item) => {
|
|
||||||
const isLocalFile = item.isLocal;
|
|
||||||
const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AttachmentItemCard
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
onRemove={() => handleRemoveItem(item)}
|
|
||||||
onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}
|
|
||||||
onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}
|
|
||||||
canMoveUp={!isLocalFile && attachmentIndex > 0}
|
|
||||||
canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttachmentListV2;
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEditorContext } from "../state";
|
import { useEditorContext } from "../state";
|
||||||
import type { EditorMetadataProps } from "../types";
|
import type { EditorMetadataProps } from "../types";
|
||||||
import AttachmentListV2 from "./AttachmentListV2";
|
import AttachmentList from "./AttachmentList";
|
||||||
import LocationDisplayV2 from "./LocationDisplayV2";
|
import LocationDisplay from "./LocationDisplay";
|
||||||
import RelationListV2 from "./RelationListV2";
|
import RelationList from "./RelationList";
|
||||||
|
|
||||||
export const EditorMetadata: FC<EditorMetadataProps> = () => {
|
export const EditorMetadata: FC<EditorMetadataProps> = () => {
|
||||||
const { state, actions, dispatch } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<AttachmentListV2
|
<AttachmentList
|
||||||
attachments={state.metadata.attachments}
|
attachments={state.metadata.attachments}
|
||||||
localFiles={state.localFiles}
|
localFiles={state.localFiles}
|
||||||
onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}
|
onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}
|
||||||
onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}
|
onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RelationListV2
|
<RelationList relations={state.metadata.relations} onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))} />
|
||||||
relations={state.metadata.relations}
|
|
||||||
onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{state.metadata.location && (
|
{state.metadata.location && (
|
||||||
<LocationDisplayV2 location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
|
<LocationDisplay location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import type { FC } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
|
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
|
|
||||||
interface LocationDisplayV2Props {
|
interface LocationDisplayProps {
|
||||||
location: Location;
|
location: Location;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationDisplayV2: FC<LocationDisplayV2Props> = ({ location, onRemove, className }) => {
|
const LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, className }) => {
|
||||||
const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
|
const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -45,4 +45,4 @@ const LocationDisplayV2: FC<LocationDisplayV2Props> = ({ location, onRemove, cla
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LocationDisplayV2;
|
export default LocationDisplay;
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import { LinkIcon, XIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
|
|
||||||
interface RelationItemCardProps {
|
|
||||||
memo: MemoRelation_Memo;
|
|
||||||
onRemove?: () => void;
|
|
||||||
parentPage?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RelationItemCard: FC<RelationItemCardProps> = ({ memo, onRemove, parentPage, className }) => {
|
|
||||||
const memoId = extractMemoIdFromName(memo.name);
|
|
||||||
|
|
||||||
if (onRemove) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="text-xs font-medium truncate flex-1" title={memo.snippet}>
|
|
||||||
{memo.snippet}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 flex items-center gap-0.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors touch-manipulation"
|
|
||||||
title="Remove"
|
|
||||||
aria-label="Remove relation"
|
|
||||||
>
|
|
||||||
<XIcon className="w-3 h-3 text-muted-foreground hover:text-destructive" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={cn(
|
|
||||||
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
to={`/${memo.name}`}
|
|
||||||
viewTransition
|
|
||||||
state={{ from: parentPage }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] font-mono px-1 py-0.5 rounded bg-muted/50 text-muted-foreground shrink-0">{memoId.slice(0, 6)}</span>
|
|
||||||
<span className="text-xs truncate flex-1" title={memo.snippet}>
|
|
||||||
{memo.snippet}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RelationItemCard;
|
|
||||||
117
web/src/components/MemoEditor/components/RelationList.tsx
Normal file
117
web/src/components/MemoEditor/components/RelationList.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { create } from "@bufbuild/protobuf";
|
||||||
|
import { LinkIcon, XIcon } from "lucide-react";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { memoServiceClient } from "@/connect";
|
||||||
|
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Memo, MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
|
import { MemoRelation_MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
|
|
||||||
|
interface RelationListProps {
|
||||||
|
relations: MemoRelation[];
|
||||||
|
onRelationsChange?: (relations: MemoRelation[]) => void;
|
||||||
|
parentPage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelationItemCard: FC<{
|
||||||
|
memo: MemoRelation["relatedMemo"];
|
||||||
|
onRemove?: () => void;
|
||||||
|
parentPage?: string;
|
||||||
|
}> = ({ memo, onRemove, parentPage }) => {
|
||||||
|
const memoId = extractMemoIdFromName(memo!.name);
|
||||||
|
|
||||||
|
if (onRemove) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-medium truncate flex-1" title={memo!.snippet}>
|
||||||
|
{memo!.snippet}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
className="p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors touch-manipulation"
|
||||||
|
title="Remove"
|
||||||
|
aria-label="Remove relation"
|
||||||
|
>
|
||||||
|
<XIcon className="w-3 h-3 text-muted-foreground hover:text-destructive" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
|
||||||
|
)}
|
||||||
|
to={`/${memo!.name}`}
|
||||||
|
viewTransition
|
||||||
|
state={{ from: parentPage }}
|
||||||
|
>
|
||||||
|
<span className="text-[10px] font-mono px-1 py-0.5 rounded bg-muted/50 text-muted-foreground shrink-0">{memoId.slice(0, 6)}</span>
|
||||||
|
<span className="text-xs truncate flex-1" title={memo!.snippet}>
|
||||||
|
{memo!.snippet}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, parentPage }) => {
|
||||||
|
const [referencingMemos, setReferencingMemos] = useState<Memo[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (relations.length > 0) {
|
||||||
|
const requests = relations.map(async (relation) => {
|
||||||
|
return await memoServiceClient.getMemo({ name: relation.relatedMemo!.name });
|
||||||
|
});
|
||||||
|
const list = await Promise.all(requests);
|
||||||
|
setReferencingMemos(list);
|
||||||
|
} else {
|
||||||
|
setReferencingMemos([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [relations]);
|
||||||
|
|
||||||
|
const handleDeleteRelation = (memoName: string) => {
|
||||||
|
if (onRelationsChange) {
|
||||||
|
onRelationsChange(relations.filter((relation) => relation.relatedMemo?.name !== memoName));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (referencingMemos.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-muted/30">
|
||||||
|
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Relations ({referencingMemos.length})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||||
|
{referencingMemos.map((memo) => (
|
||||||
|
<RelationItemCard
|
||||||
|
key={memo.name}
|
||||||
|
memo={create(MemoRelation_MemoSchema, { name: memo.name, snippet: memo.snippet })}
|
||||||
|
onRemove={() => handleDeleteRelation(memo.name)}
|
||||||
|
parentPage={parentPage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelationList;
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { create } from "@bufbuild/protobuf";
|
|
||||||
import { LinkIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { memoServiceClient } from "@/connect";
|
|
||||||
import type { Memo, MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import { MemoRelation_MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import RelationItemCard from "./RelationItemCard";
|
|
||||||
|
|
||||||
interface RelationListV2Props {
|
|
||||||
relations: MemoRelation[];
|
|
||||||
onRelationsChange?: (relations: MemoRelation[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RelationListV2: FC<RelationListV2Props> = ({ relations, onRelationsChange }) => {
|
|
||||||
const [referencingMemos, setReferencingMemos] = useState<Memo[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (relations.length > 0) {
|
|
||||||
const requests = relations.map(async (relation) => {
|
|
||||||
return await memoServiceClient.getMemo({ name: relation.relatedMemo!.name });
|
|
||||||
});
|
|
||||||
const list = await Promise.all(requests);
|
|
||||||
setReferencingMemos(list);
|
|
||||||
} else {
|
|
||||||
setReferencingMemos([]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [relations]);
|
|
||||||
|
|
||||||
const handleDeleteRelation = (memoName: string) => {
|
|
||||||
if (onRelationsChange) {
|
|
||||||
onRelationsChange(relations.filter((relation) => relation.relatedMemo?.name !== memoName));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (referencingMemos.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-muted/30">
|
|
||||||
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
|
||||||
<span className="text-xs font-medium text-muted-foreground">Relations ({referencingMemos.length})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
|
||||||
{referencingMemos.map((memo) => (
|
|
||||||
<RelationItemCard
|
|
||||||
key={memo.name}
|
|
||||||
memo={create(MemoRelation_MemoSchema, { name: memo.name, snippet: memo.snippet })}
|
|
||||||
onRemove={() => handleDeleteRelation(memo.name)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RelationListV2;
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
// UI components for MemoEditor
|
// UI components for MemoEditor
|
||||||
|
|
||||||
export { default as AttachmentItemCard } from "./AttachmentItemCard";
|
export { default as AttachmentList } from "./AttachmentList";
|
||||||
export { default as AttachmentListV2 } from "./AttachmentListV2";
|
|
||||||
export * from "./EditorContent";
|
export * from "./EditorContent";
|
||||||
export * from "./EditorMetadata";
|
export * from "./EditorMetadata";
|
||||||
export * from "./EditorToolbar";
|
export * from "./EditorToolbar";
|
||||||
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
|
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
|
||||||
export { LinkMemoDialog } from "./LinkMemoDialog";
|
export { LinkMemoDialog } from "./LinkMemoDialog";
|
||||||
export { LocationDialog } from "./LocationDialog";
|
export { LocationDialog } from "./LocationDialog";
|
||||||
export { default as LocationDisplayV2 } from "./LocationDisplayV2";
|
export { default as LocationDisplay } from "./LocationDisplay";
|
||||||
export { default as RelationItemCard } from "./RelationItemCard";
|
export { default as RelationList } from "./RelationList";
|
||||||
export { default as RelationListV2 } from "./RelationListV2";
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue