mirror of https://github.com/usememos/memos.git
144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
import { create } from "@bufbuild/protobuf";
|
|
import { LinkIcon, MilestoneIcon } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { memoServiceClient } from "@/connect";
|
|
import { cn } from "@/lib/utils";
|
|
import { Memo, MemoRelation, MemoRelation_MemoSchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
|
import { useTranslate } from "@/utils/i18n";
|
|
import MetadataCard from "./MetadataCard";
|
|
import RelationCard from "./RelationCard";
|
|
import { BaseMetadataProps } from "./types";
|
|
|
|
interface RelationListProps extends BaseMetadataProps {
|
|
relations: MemoRelation[];
|
|
currentMemoName?: string;
|
|
onRelationsChange?: (relations: MemoRelation[]) => void;
|
|
parentPage?: string;
|
|
}
|
|
|
|
function RelationList({ relations, currentMemoName, mode, onRelationsChange, parentPage, className }: RelationListProps) {
|
|
const t = useTranslate();
|
|
const [referencingMemos, setReferencingMemos] = useState<Memo[]>([]);
|
|
const [selectedTab, setSelectedTab] = useState<"referencing" | "referenced">("referencing");
|
|
|
|
// Get referencing and referenced relations
|
|
const referencingRelations = relations.filter(
|
|
(relation) =>
|
|
relation.type === MemoRelation_Type.REFERENCE &&
|
|
(mode === "edit" || relation.memo?.name === currentMemoName) &&
|
|
relation.relatedMemo?.name !== currentMemoName,
|
|
);
|
|
|
|
const referencedRelations = relations.filter(
|
|
(relation) =>
|
|
relation.type === MemoRelation_Type.REFERENCE &&
|
|
relation.memo?.name !== currentMemoName &&
|
|
relation.relatedMemo?.name === currentMemoName,
|
|
);
|
|
|
|
// Fetch full memo details for editor mode
|
|
useEffect(() => {
|
|
if (mode === "edit") {
|
|
(async () => {
|
|
if (referencingRelations.length > 0) {
|
|
const requests = referencingRelations.map(async (relation) => {
|
|
return await memoServiceClient.getMemo({ name: relation.relatedMemo!.name });
|
|
});
|
|
const list = await Promise.all(requests);
|
|
setReferencingMemos(list);
|
|
} else {
|
|
setReferencingMemos([]);
|
|
}
|
|
})();
|
|
}
|
|
}, [mode, relations]);
|
|
|
|
const handleDeleteRelation = (memoName: string) => {
|
|
if (onRelationsChange) {
|
|
onRelationsChange(relations.filter((relation) => relation.relatedMemo?.name !== memoName));
|
|
}
|
|
};
|
|
|
|
// Editor mode: Simple badge list
|
|
if (mode === "edit") {
|
|
if (referencingMemos.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="w-full flex flex-row gap-2 flex-wrap">
|
|
{referencingMemos.map((memo) => (
|
|
<RelationCard
|
|
key={memo.name}
|
|
memo={create(MemoRelation_MemoSchema, { name: memo.name, snippet: memo.snippet })}
|
|
mode="edit"
|
|
onRemove={() => handleDeleteRelation(memo.name)}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// View mode: Tabbed card with bidirectional relations
|
|
if (referencingRelations.length === 0 && referencedRelations.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Auto-select tab based on which has content
|
|
const activeTab = referencingRelations.length === 0 ? "referenced" : selectedTab;
|
|
|
|
return (
|
|
<MetadataCard className={className}>
|
|
{/* Tabs */}
|
|
<div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
|
|
{referencingRelations.length > 0 && (
|
|
<button
|
|
className={cn(
|
|
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-1 py-0.5 transition-colors",
|
|
activeTab === "referencing" && "text-foreground bg-accent",
|
|
)}
|
|
onClick={() => setSelectedTab("referencing")}
|
|
>
|
|
<LinkIcon className="w-3 h-auto shrink-0 opacity-70" />
|
|
<span>{t("common.referencing")}</span>
|
|
<span className="opacity-80">({referencingRelations.length})</span>
|
|
</button>
|
|
)}
|
|
{referencedRelations.length > 0 && (
|
|
<button
|
|
className={cn(
|
|
"w-auto flex flex-row justify-start items-center text-xs gap-0.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-1 py-0.5 transition-colors",
|
|
activeTab === "referenced" && "text-foreground bg-accent",
|
|
)}
|
|
onClick={() => setSelectedTab("referenced")}
|
|
>
|
|
<MilestoneIcon className="w-3 h-auto shrink-0 opacity-70" />
|
|
<span>{t("common.referenced-by")}</span>
|
|
<span className="opacity-80">({referencedRelations.length})</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Referencing List */}
|
|
{activeTab === "referencing" && referencingRelations.length > 0 && (
|
|
<div className="w-full flex flex-col justify-start items-start">
|
|
{referencingRelations.map((relation) => (
|
|
<RelationCard key={relation.relatedMemo!.name} memo={relation.relatedMemo!} mode="view" parentPage={parentPage} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Referenced List */}
|
|
{activeTab === "referenced" && referencedRelations.length > 0 && (
|
|
<div className="w-full flex flex-col justify-start items-start">
|
|
{referencedRelations.map((relation) => (
|
|
<RelationCard key={relation.memo!.name} memo={relation.memo!} mode="view" parentPage={parentPage} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</MetadataCard>
|
|
);
|
|
}
|
|
|
|
export default RelationList;
|