mirror of https://github.com/usememos/memos.git
chore(web): remove old Location/Attachments/Relations component files
Remove duplicate component implementations that have been replaced by the unified memo-metadata components: Removed from MemoEditor: - LocationView.tsx → replaced by memo-metadata/LocationDisplay - AttachmentListView.tsx → replaced by memo-metadata/AttachmentList - RelationListView.tsx → replaced by memo-metadata/RelationList Removed from MemoView: - MemoLocationView.tsx → replaced by memo-metadata/LocationDisplay - MemoAttachmentListView.tsx → replaced by memo-metadata/AttachmentList - MemoRelationListView.tsx → replaced by memo-metadata/RelationList This cleanup eliminates ~380 lines of duplicate code and ensures all components use the new unified architecture going forward. Note: MemoAttachment.tsx is retained as it's still used by the new AttachmentList component for rendering non-media file cards.
This commit is contained in:
parent
4e37fcfa22
commit
fe3c8e0971
|
|
@ -1,113 +0,0 @@
|
|||
import { memo, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import MemoAttachment from "./MemoAttachment";
|
||||
import PreviewImageDialog from "./PreviewImageDialog";
|
||||
|
||||
const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => {
|
||||
const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({
|
||||
open: false,
|
||||
urls: [],
|
||||
index: 0,
|
||||
});
|
||||
const mediaAttachments: Attachment[] = [];
|
||||
const otherAttachments: Attachment[] = [];
|
||||
|
||||
attachments.forEach((attachment) => {
|
||||
const type = getAttachmentType(attachment);
|
||||
if (type === "image/*" || type === "video/*") {
|
||||
mediaAttachments.push(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
otherAttachments.push(attachment);
|
||||
});
|
||||
|
||||
const handleImageClick = (imgUrl: string) => {
|
||||
const imgUrls = mediaAttachments
|
||||
.filter((attachment) => getAttachmentType(attachment) === "image/*")
|
||||
.map((attachment) => getAttachmentUrl(attachment));
|
||||
const index = imgUrls.findIndex((url) => url === imgUrl);
|
||||
setPreviewImage({ open: true, urls: imgUrls, index });
|
||||
};
|
||||
|
||||
const MediaCard = ({ attachment, className }: { attachment: Attachment; className?: string }) => {
|
||||
const type = getAttachmentType(attachment);
|
||||
const attachmentUrl = getAttachmentUrl(attachment);
|
||||
const attachmentThumbnailUrl = getAttachmentThumbnailUrl(attachment);
|
||||
|
||||
if (type === "image/*") {
|
||||
return (
|
||||
<img
|
||||
className={cn("cursor-pointer h-full w-auto rounded-lg border border-border/60 object-contain transition-colors", className)}
|
||||
src={attachmentThumbnailUrl}
|
||||
onError={(e) => {
|
||||
// Fallback to original image if thumbnail fails
|
||||
const target = e.target as HTMLImageElement;
|
||||
if (target.src.includes("?thumbnail=true")) {
|
||||
console.warn("Thumbnail failed, falling back to original image:", attachmentUrl);
|
||||
target.src = attachmentUrl;
|
||||
}
|
||||
}}
|
||||
onClick={() => handleImageClick(attachmentUrl)}
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
/>
|
||||
);
|
||||
} else if (type === "video/*") {
|
||||
return (
|
||||
<video
|
||||
className={cn(
|
||||
"cursor-pointer h-full w-auto rounded-lg border border-border/60 object-contain bg-muted transition-colors",
|
||||
className,
|
||||
)}
|
||||
preload="metadata"
|
||||
crossOrigin="anonymous"
|
||||
src={attachmentUrl}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
const MediaList = ({ attachments = [] }: { attachments: Attachment[] }) => {
|
||||
const cards = attachments.map((attachment) => (
|
||||
<div key={attachment.name} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
|
||||
<MediaCard className="max-h-64 grow" attachment={attachment} />
|
||||
</div>
|
||||
));
|
||||
|
||||
return <div className="w-full flex flex-row justify-start overflow-auto gap-2">{cards}</div>;
|
||||
};
|
||||
|
||||
const OtherList = ({ attachments = [] }: { attachments: Attachment[] }) => {
|
||||
if (attachments.length === 0) return <></>;
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
||||
{otherAttachments.map((attachment) => (
|
||||
<MemoAttachment key={attachment.name} attachment={attachment} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{mediaAttachments.length > 0 && <MediaList attachments={mediaAttachments} />}
|
||||
<OtherList attachments={otherAttachments} />
|
||||
|
||||
<PreviewImageDialog
|
||||
open={previewImage.open}
|
||||
onOpenChange={(open) => setPreviewImage((prev) => ({ ...prev, open }))}
|
||||
imgUrls={previewImage.urls}
|
||||
initialIndex={previewImage.index}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MemoAttachmentListView);
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { FileIcon, XIcon } from "lucide-react";
|
||||
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||
import { getAttachmentThumbnailUrl, getAttachmentType } from "@/utils/attachment";
|
||||
import SortableItem from "./SortableItem";
|
||||
|
||||
interface Props {
|
||||
attachmentList: Attachment[];
|
||||
setAttachmentList: (attachmentList: Attachment[]) => void;
|
||||
}
|
||||
|
||||
const AttachmentListView = (props: Props) => {
|
||||
const { attachmentList, setAttachmentList } = props;
|
||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
||||
|
||||
const handleDeleteAttachment = async (name: string) => {
|
||||
setAttachmentList(attachmentList.filter((attachment) => attachment.name !== name));
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = attachmentList.findIndex((attachment) => attachment.name === active.id);
|
||||
const newIndex = attachmentList.findIndex((attachment) => attachment.name === over.id);
|
||||
|
||||
setAttachmentList(arrayMove(attachmentList, oldIndex, newIndex));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={attachmentList.map((attachment) => attachment.name)} strategy={verticalListSortingStrategy}>
|
||||
{attachmentList.length > 0 && (
|
||||
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2 max-h-[50vh] overflow-y-auto">
|
||||
{attachmentList.map((attachment) => {
|
||||
return (
|
||||
<div
|
||||
key={attachment.name}
|
||||
className="group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent"
|
||||
>
|
||||
<SortableItem id={attachment.name} className="flex items-center gap-1.5 min-w-0">
|
||||
{getAttachmentType(attachment) === "image/*" ? (
|
||||
<img
|
||||
src={getAttachmentThumbnailUrl(attachment)}
|
||||
alt={attachment.filename}
|
||||
className="w-5 h-5 shrink-0 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<FileIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate max-w-[160px]">{attachment.filename}</span>
|
||||
</SortableItem>
|
||||
<button
|
||||
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
|
||||
onClick={() => handleDeleteAttachment(attachment.name)}
|
||||
>
|
||||
<XIcon className="w-3 h-3 text-muted-foreground hover:text-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentListView;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { MapPinIcon, XIcon } from "lucide-react";
|
||||
import { Location } from "@/types/proto/api/v1/memo_service";
|
||||
|
||||
interface Props {
|
||||
location?: Location;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const LocationView = (props: Props) => {
|
||||
if (!props.location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row flex-wrap gap-2 mt-2">
|
||||
<div className="group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent">
|
||||
<MapPinIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate max-w-[160px]">{props.location.placeholder}</span>
|
||||
<button
|
||||
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.onRemove();
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-3 h-3 text-muted-foreground hover:text-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationView;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { LinkIcon, XIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { memoStore } from "@/store";
|
||||
import { Memo, MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_service";
|
||||
|
||||
interface Props {
|
||||
relationList: MemoRelation[];
|
||||
setRelationList: (relationList: MemoRelation[]) => void;
|
||||
}
|
||||
|
||||
const RelationListView = observer((props: Props) => {
|
||||
const { relationList, setRelationList } = props;
|
||||
const [referencingMemoList, setReferencingMemoList] = useState<Memo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const requests = relationList
|
||||
.filter((relation) => relation.type === MemoRelation_Type.REFERENCE)
|
||||
.map(async (relation) => {
|
||||
return await memoStore.getOrFetchMemoByName(relation.relatedMemo!.name, { skipStore: true });
|
||||
});
|
||||
const list = await Promise.all(requests);
|
||||
setReferencingMemoList(list);
|
||||
})();
|
||||
}, [relationList]);
|
||||
|
||||
const handleDeleteRelation = async (memo: Memo) => {
|
||||
setRelationList(relationList.filter((relation) => relation.relatedMemo?.name !== memo.name));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{referencingMemoList.length > 0 && (
|
||||
<div className="w-full flex flex-row gap-2 mt-2 flex-wrap">
|
||||
{referencingMemoList.map((memo) => {
|
||||
return (
|
||||
<div
|
||||
key={memo.name}
|
||||
className="group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent cursor-pointer"
|
||||
onClick={() => handleDeleteRelation(memo)}
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate max-w-[160px]">{memo.snippet}</span>
|
||||
<XIcon className="w-3 h-3 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default RelationListView;
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { LatLng } from "leaflet";
|
||||
import { MapPinIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Location } from "@/types/proto/api/v1/memo_service";
|
||||
import LeafletMap from "./LeafletMap";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
}
|
||||
|
||||
const MemoLocationView: React.FC<Props> = (props: Props) => {
|
||||
const { location } = props;
|
||||
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<p className="w-full flex flex-row gap-0.5 items-center text-muted-foreground hover:text-foreground cursor-pointer transition-colors">
|
||||
<MapPinIcon className="w-4 h-auto shrink-0" />
|
||||
<span className="text-sm font-normal text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{location.placeholder ? location.placeholder : `[${location.latitude}, ${location.longitude}]`}
|
||||
</span>
|
||||
</p>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start">
|
||||
<div className="min-w-80 sm:w-lg flex flex-col justify-start items-start">
|
||||
<LeafletMap latlng={new LatLng(location.latitude, location.longitude)} readonly={true} />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoLocationView;
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import { LinkIcon, MilestoneIcon } from "lucide-react";
|
||||
import { memo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { extractMemoIdFromName } from "@/store/common";
|
||||
import { Memo, MemoRelation } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
relations: MemoRelation[];
|
||||
parentPage?: string;
|
||||
}
|
||||
|
||||
const MemoRelationListView = (props: Props) => {
|
||||
const t = useTranslate();
|
||||
const { memo, relations: relationList, parentPage } = props;
|
||||
const referencingMemoList = relationList
|
||||
.filter((relation) => relation.memo?.name === memo.name && relation.relatedMemo?.name !== memo.name)
|
||||
.map((relation) => relation.relatedMemo!);
|
||||
const referencedMemoList = relationList
|
||||
.filter((relation) => relation.memo?.name !== memo.name && relation.relatedMemo?.name === memo.name)
|
||||
.map((relation) => relation.memo!);
|
||||
const [selectedTab, setSelectedTab] = useState<"referencing" | "referenced">(
|
||||
referencingMemoList.length === 0 ? "referenced" : "referencing",
|
||||
);
|
||||
|
||||
if (referencingMemoList.length + referencedMemoList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col justify-start items-start w-full px-2 pt-2 pb-1.5 bg-muted/50 rounded-lg border border-border">
|
||||
<div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
|
||||
{referencingMemoList.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",
|
||||
selectedTab === "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">({referencingMemoList.length})</span>
|
||||
</button>
|
||||
)}
|
||||
{referencedMemoList.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",
|
||||
selectedTab === "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">({referencedMemoList.length})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{selectedTab === "referencing" && referencingMemoList.length > 0 && (
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
{referencingMemoList.map((memo) => {
|
||||
return (
|
||||
<Link
|
||||
key={memo.name}
|
||||
className="w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors"
|
||||
to={`/${memo.name}`}
|
||||
viewTransition
|
||||
state={{
|
||||
from: parentPage,
|
||||
}}
|
||||
>
|
||||
<span className="text-xs opacity-60 leading-4 border border-border font-mono px-1 rounded-full mr-1">
|
||||
{extractMemoIdFromName(memo.name).slice(0, 6)}
|
||||
</span>
|
||||
<span className="truncate">{memo.snippet}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{selectedTab === "referenced" && referencedMemoList.length > 0 && (
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
{referencedMemoList.map((memo) => {
|
||||
return (
|
||||
<Link
|
||||
key={memo.name}
|
||||
className="w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors"
|
||||
to={`/${memo.name}`}
|
||||
viewTransition
|
||||
state={{
|
||||
from: parentPage,
|
||||
}}
|
||||
>
|
||||
<span className="text-xs opacity-60 leading-4 border border-border font-mono px-1 rounded-full mr-1">
|
||||
{extractMemoIdFromName(memo.name).slice(0, 6)}
|
||||
</span>
|
||||
<span className="truncate">{memo.snippet}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MemoRelationListView);
|
||||
Loading…
Reference in New Issue