Unverified Commit 41778980 authored by memoclaw's avatar memoclaw Committed by GitHub

refactor(web): consolidate memo metadata components into MemoMetadata (#5755)

Co-authored-by: 's avatarmemoclaw <265580040+memoclaw@users.noreply.github.com>
Co-authored-by: 's avatarCopilot <223556219+Copilot@users.noreply.github.com>
parent ac077ac3
import { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentUrl, isMidiFile } from "@/utils/attachment";
import AttachmentIcon from "./AttachmentIcon";
interface Props {
attachment: Attachment;
className?: string;
}
const MemoAttachment: React.FC<Props> = (props: Props) => {
const { className, attachment } = props;
const attachmentUrl = getAttachmentUrl(attachment);
const handlePreviewBtnClick = () => {
window.open(attachmentUrl);
};
return (
<div
className={`w-auto flex flex-row justify-start items-center text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors ${className}`}
>
{attachment.type.startsWith("audio") && !isMidiFile(attachment.type) ? (
<audio src={attachmentUrl} controls></audio>
) : (
<>
<AttachmentIcon className="w-4! h-4! mr-1" attachment={attachment} />
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
{attachment.filename}
</span>
</>
)}
</div>
);
};
export default MemoAttachment;
...@@ -3,6 +3,7 @@ import { uniqBy } from "lodash-es"; ...@@ -3,6 +3,7 @@ import { uniqBy } from "lodash-es";
import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react"; import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useDebounce } from "react-use"; import { useDebounce } from "react-use";
import { LinkMemoDialog, LocationDialog } from "@/components/MemoMetadata";
import { useReverseGeocoding } from "@/components/map"; import { useReverseGeocoding } from "@/components/map";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -17,7 +18,6 @@ import { ...@@ -17,7 +18,6 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { LinkMemoDialog, LocationDialog } from "../components";
import { useFileUpload, useLinkMemo, useLocation } from "../hooks"; import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
import { useEditorContext } from "../state"; import { useEditorContext } from "../state";
import type { InsertMenuProps } from "../types"; import type { InsertMenuProps } from "../types";
......
import type { FC } from "react"; import type { FC } from "react";
import { AttachmentListEditor, LocationDisplayEditor, RelationListEditor } from "@/components/MemoMetadata";
import { useEditorContext } from "../state"; import { useEditorContext } from "../state";
import type { EditorMetadataProps } from "../types"; import type { EditorMetadataProps } from "../types";
import AttachmentList from "./AttachmentList";
import LocationDisplay from "./LocationDisplay";
import RelationList from "./RelationList";
export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => { export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {
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">
<AttachmentList <AttachmentListEditor
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))}
/> />
<RelationList <RelationListEditor
relations={state.metadata.relations} relations={state.metadata.relations}
onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))} onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}
memoName={memoName} memoName={memoName}
/> />
{state.metadata.location && ( {state.metadata.location && (
<LocationDisplay location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} /> <LocationDisplayEditor location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
)} )}
</div> </div>
); );
......
// UI components for MemoEditor // UI components for MemoEditor
export { default as AttachmentList } from "./AttachmentList";
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 { LocationDialog } from "./LocationDialog";
export { default as LocationDisplay } from "./LocationDisplay";
export { default as RelationList } from "./RelationList";
export { TimestampPopover } from "./TimestampPopover"; export { TimestampPopover } from "./TimestampPopover";
import type { LatLng } from "leaflet";
import type { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb"; import type { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { EditorRefActions } from "../Editor"; import type { EditorRefActions } from "../Editor";
import type { Command } from "../Editor/commands"; import type { Command } from "../Editor/commands";
import type { LocationState } from "./insert-menu";
export interface MemoEditorProps { export interface MemoEditorProps {
className?: string; className?: string;
...@@ -41,28 +39,6 @@ export interface FocusModeExitButtonProps { ...@@ -41,28 +39,6 @@ export interface FocusModeExitButtonProps {
title: string; title: string;
} }
export interface LinkMemoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchText: string;
onSearchChange: (text: string) => void;
filteredMemos: Memo[];
isFetching: boolean;
onSelectMemo: (memo: Memo) => void;
isAlreadyLinked: (memoName: string) => boolean;
}
export interface LocationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
state: LocationState;
onPositionChange: (position: LatLng) => void;
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
onPlaceholderChange: (placeholder: string) => void;
onCancel: () => void;
onConfirm: () => void;
}
export interface InsertMenuProps { export interface InsertMenuProps {
isUploading?: boolean; isUploading?: boolean;
location?: Location; location?: Location;
......
...@@ -8,8 +8,6 @@ export type { ...@@ -8,8 +8,6 @@ export type {
FocusModeExitButtonProps, FocusModeExitButtonProps,
FocusModeOverlayProps, FocusModeOverlayProps,
InsertMenuProps, InsertMenuProps,
LinkMemoDialogProps,
LocationDialogProps,
MemoEditorProps, MemoEditorProps,
SlashCommandsProps, SlashCommandsProps,
TagSuggestionsProps, TagSuggestionsProps,
......
import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import type { LocalFile } from "@/components/MemoEditor/types/attachment";
import { toAttachmentItems } from "@/components/MemoEditor/types/attachment";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import type { LocalFile } from "../types/attachment"; import SectionHeader from "../SectionHeader";
import { toAttachmentItems } from "../types/attachment";
interface AttachmentListProps { interface AttachmentListEditorProps {
attachments: Attachment[]; attachments: Attachment[];
localFiles?: LocalFile[]; localFiles?: LocalFile[];
onAttachmentsChange?: (attachments: Attachment[]) => void; onAttachmentsChange?: (attachments: Attachment[]) => void;
...@@ -100,7 +101,7 @@ const AttachmentItemCard: FC<{ ...@@ -100,7 +101,7 @@ const AttachmentItemCard: FC<{
); );
}; };
const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => { const AttachmentListEditor: FC<AttachmentListEditorProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
if (attachments.length === 0 && localFiles.length === 0) { if (attachments.length === 0 && localFiles.length === 0) {
return null; return null;
} }
...@@ -139,10 +140,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], ...@@ -139,10 +140,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
return ( return (
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden"> <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 border-b border-border bg-muted/30"> <SectionHeader icon={PaperclipIcon} title="Attachments" count={items.length} />
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
</div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5"> <div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
{items.map((item) => { {items.map((item) => {
...@@ -166,4 +164,4 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], ...@@ -166,4 +164,4 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
); );
}; };
export default AttachmentList; export default AttachmentListEditor;
import { FileAudioIcon, FileIcon, PaperclipIcon } from "lucide-react"; import { FileAudioIcon, FileIcon, PaperclipIcon } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import { useMemoViewContext } from "../../MemoViewContext"; import SectionHeader from "../SectionHeader";
import AttachmentCard from "./AttachmentCard"; import AttachmentCard from "./AttachmentCard";
import SectionHeader from "./SectionHeader";
interface AttachmentListProps { interface AttachmentListViewProps {
attachments: Attachment[]; attachments: Attachment[];
onImagePreview?: (urls: string[], index: number) => void;
} }
const isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "image/*"; const isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "image/*";
...@@ -79,19 +80,24 @@ const AudioItem = ({ attachment }: { attachment: Attachment }) => { ...@@ -79,19 +80,24 @@ const AudioItem = ({ attachment }: { attachment: Attachment }) => {
interface VisualItemProps { interface VisualItemProps {
attachment: Attachment; attachment: Attachment;
onImageClick: (url: string) => void; onImageClick?: (url: string) => void;
} }
const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => { const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {
const isInteractive = isImageAttachment(attachment) && Boolean(onImageClick);
const handleClick = () => { const handleClick = () => {
if (isImageAttachment(attachment)) { if (isInteractive) {
onImageClick(getAttachmentUrl(attachment)); onImageClick?.(getAttachmentUrl(attachment));
} }
}; };
return ( return (
<div <div
className="aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all cursor-pointer group" className={cn(
"aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all group",
isInteractive && "cursor-pointer",
)}
onClick={handleClick} onClick={handleClick}
> >
<AttachmentCard attachment={attachment} className="rounded-none" /> <AttachmentCard attachment={attachment} className="rounded-none" />
...@@ -99,7 +105,7 @@ const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => { ...@@ -99,7 +105,7 @@ const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {
); );
}; };
const VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick: (url: string) => void }) => ( const VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick?: (url: string) => void }) => (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{attachments.map((attachment) => ( {attachments.map((attachment) => (
<VisualItem key={attachment.name} attachment={attachment} onImageClick={onImageClick} /> <VisualItem key={attachment.name} attachment={attachment} onImageClick={onImageClick} />
...@@ -127,9 +133,7 @@ const DocsList = ({ attachments }: { attachments: Attachment[] }) => ( ...@@ -127,9 +133,7 @@ const DocsList = ({ attachments }: { attachments: Attachment[] }) => (
const Divider = () => <div className="border-t mt-1 border-border opacity-60" />; const Divider = () => <div className="border-t mt-1 border-border opacity-60" />;
const AttachmentList = ({ attachments }: AttachmentListProps) => { const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewProps) => {
const { openPreview } = useMemoViewContext();
const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]); const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]);
const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]); const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]);
...@@ -141,7 +145,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { ...@@ -141,7 +145,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
const handleImageClick = (imgUrl: string) => { const handleImageClick = (imgUrl: string) => {
const index = imageUrls.findIndex((url) => url === imgUrl); const index = imageUrls.findIndex((url) => url === imgUrl);
openPreview(imageUrls, index >= 0 ? index : 0); onImagePreview?.(imageUrls, index >= 0 ? index : 0);
}; };
const sections = [visual.length > 0, audio.length > 0, docs.length > 0]; const sections = [visual.length > 0, audio.length > 0, docs.length > 0];
...@@ -166,4 +170,4 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { ...@@ -166,4 +170,4 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
); );
}; };
export default AttachmentList; export default AttachmentListView;
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentListEditor } from "./AttachmentListEditor";
export { default as AttachmentListView } from "./AttachmentListView";
import type { LatLng } from "leaflet";
import type { LocationState } from "@/components/MemoEditor/types/insert-menu";
import { LocationPicker } from "@/components/map"; import { LocationPicker } from "@/components/map";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
...@@ -6,7 +8,17 @@ import { Label } from "@/components/ui/label"; ...@@ -6,7 +8,17 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { VisuallyHidden } from "@/components/ui/visually-hidden"; import { VisuallyHidden } from "@/components/ui/visually-hidden";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import type { LocationDialogProps } from "../types";
interface LocationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
state: LocationState;
onPositionChange: (position: LatLng) => void;
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
onPlaceholderChange: (placeholder: string) => void;
onCancel: () => void;
onConfirm: () => void;
}
export const LocationDialog = ({ export const LocationDialog = ({
open, open,
......
...@@ -3,13 +3,13 @@ import type { FC } from "react"; ...@@ -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 LocationDisplayProps { interface LocationDisplayEditorProps {
location: Location; location: Location;
onRemove?: () => void; onRemove?: () => void;
className?: string; className?: string;
} }
const LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, className }) => { const LocationDisplayEditor: FC<LocationDisplayEditorProps> = ({ 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 LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, classNa ...@@ -45,4 +45,4 @@ const LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, classNa
); );
}; };
export default LocationDisplay; export default LocationDisplayEditor;
...@@ -2,16 +2,16 @@ import { LatLng } from "leaflet"; ...@@ -2,16 +2,16 @@ import { LatLng } from "leaflet";
import { MapPinIcon } from "lucide-react"; import { MapPinIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { LocationPicker } from "@/components/map"; import { LocationPicker } from "@/components/map";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
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";
import { Popover, PopoverContent, PopoverTrigger } from "../../../ui/popover";
interface LocationDisplayProps { interface LocationDisplayViewProps {
location?: Location; location?: Location;
className?: string; className?: string;
} }
const LocationDisplay = ({ location, className }: LocationDisplayProps) => { const LocationDisplayView = ({ location, className }: LocationDisplayViewProps) => {
const [popoverOpen, setPopoverOpen] = useState<boolean>(false); const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
if (!location) { if (!location) {
...@@ -49,4 +49,4 @@ const LocationDisplay = ({ location, className }: LocationDisplayProps) => { ...@@ -49,4 +49,4 @@ const LocationDisplay = ({ location, className }: LocationDisplayProps) => {
); );
}; };
export default LocationDisplay; export default LocationDisplayView;
export { LocationDialog } from "./LocationDialog";
export { default as LocationDisplayEditor } from "./LocationDisplayEditor";
export { default as LocationDisplayView } from "./LocationDisplayView";
...@@ -6,8 +6,19 @@ import { Input } from "@/components/ui/input"; ...@@ -6,8 +6,19 @@ import { Input } from "@/components/ui/input";
import { VisuallyHidden } from "@/components/ui/visually-hidden"; import { VisuallyHidden } from "@/components/ui/visually-hidden";
import { extractMemoIdFromName } from "@/helpers/resource-names"; import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import type { LinkMemoDialogProps } from "../types";
interface LinkMemoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchText: string;
onSearchChange: (text: string) => void;
filteredMemos: Memo[];
isFetching: boolean;
onSelectMemo: (memo: Memo) => void;
isAlreadyLinked: (memoName: string) => boolean;
}
export const LinkMemoDialog = ({ export const LinkMemoDialog = ({
open, open,
......
import MemoSnippetLink from "@/components/MemoView/components/MemoSnippetLink";
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb"; import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
import MemoSnippetLink from "../MemoSnippetLink";
interface RelationCardProps { interface RelationCardProps {
memo: MemoRelation_Memo; memo: MemoRelation_Memo;
......
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { LinkIcon, XIcon } from "lucide-react"; import { LinkIcon, XIcon } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import RelationCard from "@/components/MemoView/components/metadata/RelationCard";
import { memoServiceClient } from "@/connect"; import { memoServiceClient } from "@/connect";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Memo, MemoRelation_MemoSchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; import { MemoRelation_Memo, MemoRelation_MemoSchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import SectionHeader from "../SectionHeader";
import RelationCard from "./RelationCard";
interface RelationListProps { interface RelationListEditorProps {
relations: MemoRelation[]; relations: MemoRelation[];
onRelationsChange?: (relations: MemoRelation[]) => void; onRelationsChange?: (relations: MemoRelation[]) => void;
parentPage?: string; parentPage?: string;
...@@ -38,9 +39,10 @@ const RelationItemCard: FC<{ ...@@ -38,9 +39,10 @@ const RelationItemCard: FC<{
); );
}; };
const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, parentPage, memoName }) => { const RelationListEditor: FC<RelationListEditorProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
const referenceRelations = relations.filter( const referenceRelations = useMemo(
(r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName), () => relations.filter((r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName)),
[relations, memoName],
); );
const [fetchedMemos, setFetchedMemos] = useState<Record<string, MemoRelation_Memo>>({}); const [fetchedMemos, setFetchedMemos] = useState<Record<string, MemoRelation_Memo>>({});
...@@ -76,10 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par ...@@ -76,10 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
return ( return (
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden"> <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 border-b border-border bg-muted/30"> <SectionHeader icon={LinkIcon} title="Relations" count={referenceRelations.length} />
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
</div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5"> <div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
{referenceRelations.map((relation) => { {referenceRelations.map((relation) => {
...@@ -92,4 +91,4 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par ...@@ -92,4 +91,4 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
); );
}; };
export default RelationList; export default RelationListEditor;
...@@ -4,17 +4,17 @@ import { cn } from "@/lib/utils"; ...@@ -4,17 +4,17 @@ import { cn } from "@/lib/utils";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import SectionHeader from "../SectionHeader";
import RelationCard from "./RelationCard"; import RelationCard from "./RelationCard";
import SectionHeader from "./SectionHeader";
interface RelationListProps { interface RelationListViewProps {
relations: MemoRelation[]; relations: MemoRelation[];
currentMemoName?: string; currentMemoName?: string;
parentPage?: string; parentPage?: string;
className?: string; className?: string;
} }
function RelationList({ relations, currentMemoName, parentPage, className }: RelationListProps) { function RelationListView({ relations, currentMemoName, parentPage, className }: RelationListViewProps) {
const t = useTranslate(); const t = useTranslate();
const [activeTab, setActiveTab] = useState<"referencing" | "referenced">("referencing"); const [activeTab, setActiveTab] = useState<"referencing" | "referenced">("referencing");
...@@ -81,4 +81,4 @@ function RelationList({ relations, currentMemoName, parentPage, className }: Rel ...@@ -81,4 +81,4 @@ function RelationList({ relations, currentMemoName, parentPage, className }: Rel
); );
} }
export default RelationList; export default RelationListView;
export { LinkMemoDialog } from "./LinkMemoDialog";
export { default as RelationCard } from "./RelationCard";
export { default as RelationListEditor } from "./RelationListEditor";
export { default as RelationListView } from "./RelationListView";
// Consolidated memo metadata components for attachments, locations, and relations.
export { AttachmentCard, AttachmentListEditor, AttachmentListView } from "./Attachment";
export { LocationDialog, LocationDisplayEditor, LocationDisplayView } from "./Location";
export { LinkMemoDialog, RelationCard, RelationListEditor, RelationListView } from "./Relation";
export { default as SectionHeader } from "./SectionHeader";
import { AttachmentListView, LocationDisplayView, RelationListView } from "@/components/MemoMetadata";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
...@@ -6,7 +7,6 @@ import { MemoReactionListView } from "../../MemoReactionListView"; ...@@ -6,7 +7,6 @@ import { MemoReactionListView } from "../../MemoReactionListView";
import { useMemoHandlers } from "../hooks"; import { useMemoHandlers } from "../hooks";
import { useMemoViewContext } from "../MemoViewContext"; import { useMemoViewContext } from "../MemoViewContext";
import type { MemoBodyProps } from "../types"; import type { MemoBodyProps } from "../types";
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
const NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => { const NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {
const t = useTranslate(); const t = useTranslate();
...@@ -44,9 +44,9 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => { ...@@ -44,9 +44,9 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
onDoubleClick={handleMemoContentDoubleClick} onDoubleClick={handleMemoContentDoubleClick}
compact={memo.pinned ? false : compact} // Always show full content when pinned compact={memo.pinned ? false : compact} // Always show full content when pinned
/> />
<AttachmentList attachments={memo.attachments} /> <AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
<RelationList relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} /> <RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
{memo.location && <LocationDisplay location={memo.location} />} {memo.location && <LocationDisplayView location={memo.location} />}
<MemoReactionListView memo={memo} reactions={memo.reactions} /> <MemoReactionListView memo={memo} reactions={memo.reactions} />
</div> </div>
......
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentList } from "./AttachmentList";
export { default as LocationDisplay } from "./LocationDisplay";
export { default as RelationCard } from "./RelationCard";
export { default as RelationList } from "./RelationList";
...@@ -4,7 +4,9 @@ import { Code, ConnectError } from "@connectrpc/connect"; ...@@ -4,7 +4,9 @@ import { Code, ConnectError } from "@connectrpc/connect";
import { AlertCircleIcon } from "lucide-react"; import { AlertCircleIcon } from "lucide-react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import MemoContent from "@/components/MemoContent"; import MemoContent from "@/components/MemoContent";
import AttachmentList from "@/components/MemoView/components/metadata/AttachmentList"; import { AttachmentListView } from "@/components/MemoMetadata";
import { useImagePreview } from "@/components/MemoView/hooks";
import PreviewImageDialog from "@/components/PreviewImageDialog";
import UserAvatar from "@/components/UserAvatar"; import UserAvatar from "@/components/UserAvatar";
import { useSharedMemo } from "@/hooks/useMemoShareQueries"; import { useSharedMemo } from "@/hooks/useMemoShareQueries";
import { useUser } from "@/hooks/useUserQueries"; import { useUser } from "@/hooks/useUserQueries";
...@@ -22,6 +24,7 @@ function withShareAttachmentLinks(attachments: Attachment[], token: string): Att ...@@ -22,6 +24,7 @@ function withShareAttachmentLinks(attachments: Attachment[], token: string): Att
const SharedMemo = () => { const SharedMemo = () => {
const t = useTranslate(); const t = useTranslate();
const { token = "" } = useParams<{ token: string }>(); const { token = "" } = useParams<{ token: string }>();
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token }); const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token });
const { data: creator } = useUser(memo?.creator ?? "", { enabled: !!memo?.creator }); const { data: creator } = useUser(memo?.creator ?? "", { enabled: !!memo?.creator });
...@@ -64,8 +67,17 @@ const SharedMemo = () => { ...@@ -64,8 +67,17 @@ const SharedMemo = () => {
<div className="relative flex flex-col items-start gap-2 rounded-lg border border-border bg-card px-4 py-3 text-card-foreground"> <div className="relative flex flex-col items-start gap-2 rounded-lg border border-border bg-card px-4 py-3 text-card-foreground">
<MemoContent content={memo.content} /> <MemoContent content={memo.content} />
{memo.attachments.length > 0 && <AttachmentList attachments={withShareAttachmentLinks(memo.attachments, token)} />} {memo.attachments.length > 0 && (
<AttachmentListView attachments={withShareAttachmentLinks(memo.attachments, token)} onImagePreview={openPreview} />
)}
</div> </div>
<PreviewImageDialog
open={previewState.open}
onOpenChange={setPreviewOpen}
imgUrls={previewState.urls}
initialIndex={previewState.index}
/>
</div> </div>
); );
}; };
......
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