refactor(web): consolidate memo metadata components into MemoMetadata (#5755)
Co-authored-by: memoclaw <265580040+memoclaw@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ac077ac3d3
commit
4177898054
25 changed files with 110 additions and 133 deletions
|
|
@ -1,36 +0,0 @@
|
|||
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";
|
|||
import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import { LinkMemoDialog, LocationDialog } from "@/components/MemoMetadata";
|
||||
import { useReverseGeocoding } from "@/components/map";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -17,7 +18,6 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { LinkMemoDialog, LocationDialog } from "../components";
|
||||
import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
|
||||
import { useEditorContext } from "../state";
|
||||
import type { InsertMenuProps } from "../types";
|
||||
|
|
|
|||
|
|
@ -1,30 +1,28 @@
|
|||
import type { FC } from "react";
|
||||
import { AttachmentListEditor, LocationDisplayEditor, RelationListEditor } from "@/components/MemoMetadata";
|
||||
import { useEditorContext } from "../state";
|
||||
import type { EditorMetadataProps } from "../types";
|
||||
import AttachmentList from "./AttachmentList";
|
||||
import LocationDisplay from "./LocationDisplay";
|
||||
import RelationList from "./RelationList";
|
||||
|
||||
export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {
|
||||
const { state, actions, dispatch } = useEditorContext();
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<AttachmentList
|
||||
<AttachmentListEditor
|
||||
attachments={state.metadata.attachments}
|
||||
localFiles={state.localFiles}
|
||||
onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}
|
||||
onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}
|
||||
/>
|
||||
|
||||
<RelationList
|
||||
<RelationListEditor
|
||||
relations={state.metadata.relations}
|
||||
onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}
|
||||
memoName={memoName}
|
||||
/>
|
||||
|
||||
{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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
// UI components for MemoEditor
|
||||
|
||||
export { default as AttachmentList } from "./AttachmentList";
|
||||
export * from "./EditorContent";
|
||||
export * from "./EditorMetadata";
|
||||
export * from "./EditorToolbar";
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import type { LatLng } from "leaflet";
|
||||
import type { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { EditorRefActions } from "../Editor";
|
||||
import type { Command } from "../Editor/commands";
|
||||
import type { LocationState } from "./insert-menu";
|
||||
|
||||
export interface MemoEditorProps {
|
||||
className?: string;
|
||||
|
|
@ -41,28 +39,6 @@ export interface FocusModeExitButtonProps {
|
|||
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 {
|
||||
isUploading?: boolean;
|
||||
location?: Location;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export type {
|
|||
FocusModeExitButtonProps,
|
||||
FocusModeOverlayProps,
|
||||
InsertMenuProps,
|
||||
LinkMemoDialogProps,
|
||||
LocationDialogProps,
|
||||
MemoEditorProps,
|
||||
SlashCommandsProps,
|
||||
TagSuggestionsProps,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "lucide-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 type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
|
||||
import type { LocalFile } from "../types/attachment";
|
||||
import { toAttachmentItems } from "../types/attachment";
|
||||
import SectionHeader from "../SectionHeader";
|
||||
|
||||
interface AttachmentListProps {
|
||||
interface AttachmentListEditorProps {
|
||||
attachments: Attachment[];
|
||||
localFiles?: LocalFile[];
|
||||
onAttachmentsChange?: (attachments: Attachment[]) => void;
|
||||
|
|
@ -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) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -139,10 +140,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
|
|||
|
||||
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 border-b border-border bg-muted/30">
|
||||
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
|
||||
</div>
|
||||
<SectionHeader icon={PaperclipIcon} title="Attachments" count={items.length} />
|
||||
|
||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||
{items.map((item) => {
|
||||
|
|
@ -166,4 +164,4 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
|
|||
);
|
||||
};
|
||||
|
||||
export default AttachmentList;
|
||||
export default AttachmentListEditor;
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { FileAudioIcon, FileIcon, PaperclipIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
|
||||
import { useMemoViewContext } from "../../MemoViewContext";
|
||||
import SectionHeader from "../SectionHeader";
|
||||
import AttachmentCard from "./AttachmentCard";
|
||||
import SectionHeader from "./SectionHeader";
|
||||
|
||||
interface AttachmentListProps {
|
||||
interface AttachmentListViewProps {
|
||||
attachments: Attachment[];
|
||||
onImagePreview?: (urls: string[], index: number) => void;
|
||||
}
|
||||
|
||||
const isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "image/*";
|
||||
|
|
@ -79,19 +80,24 @@ const AudioItem = ({ attachment }: { attachment: Attachment }) => {
|
|||
|
||||
interface VisualItemProps {
|
||||
attachment: Attachment;
|
||||
onImageClick: (url: string) => void;
|
||||
onImageClick?: (url: string) => void;
|
||||
}
|
||||
|
||||
const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {
|
||||
const isInteractive = isImageAttachment(attachment) && Boolean(onImageClick);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isImageAttachment(attachment)) {
|
||||
onImageClick(getAttachmentUrl(attachment));
|
||||
if (isInteractive) {
|
||||
onImageClick?.(getAttachmentUrl(attachment));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<AttachmentCard attachment={attachment} className="rounded-none" />
|
||||
|
|
@ -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">
|
||||
{attachments.map((attachment) => (
|
||||
<VisualItem key={attachment.name} attachment={attachment} onImageClick={onImageClick} />
|
||||
|
|
@ -127,9 +133,7 @@ const DocsList = ({ attachments }: { attachments: Attachment[] }) => (
|
|||
|
||||
const Divider = () => <div className="border-t mt-1 border-border opacity-60" />;
|
||||
|
||||
const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
||||
const { openPreview } = useMemoViewContext();
|
||||
|
||||
const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewProps) => {
|
||||
const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]);
|
||||
|
||||
const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]);
|
||||
|
|
@ -141,7 +145,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
|||
|
||||
const handleImageClick = (imgUrl: string) => {
|
||||
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];
|
||||
|
|
@ -166,4 +170,4 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default AttachmentList;
|
||||
export default AttachmentListView;
|
||||
3
web/src/components/MemoMetadata/Attachment/index.ts
Normal file
3
web/src/components/MemoMetadata/Attachment/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as AttachmentCard } from "./AttachmentCard";
|
||||
export { default as AttachmentListEditor } from "./AttachmentListEditor";
|
||||
export { default as AttachmentListView } from "./AttachmentListView";
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import type { LatLng } from "leaflet";
|
||||
import type { LocationState } from "@/components/MemoEditor/types/insert-menu";
|
||||
import { LocationPicker } from "@/components/map";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
||||
|
|
@ -6,7 +8,17 @@ import { Label } from "@/components/ui/label";
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { VisuallyHidden } from "@/components/ui/visually-hidden";
|
||||
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 = ({
|
||||
open,
|
||||
|
|
@ -3,13 +3,13 @@ import type { FC } from "react";
|
|||
import { cn } from "@/lib/utils";
|
||||
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
interface LocationDisplayProps {
|
||||
interface LocationDisplayEditorProps {
|
||||
location: Location;
|
||||
onRemove?: () => void;
|
||||
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)}`;
|
||||
|
||||
return (
|
||||
|
|
@ -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";
|
|||
import { MapPinIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { LocationPicker } from "@/components/map";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../../../ui/popover";
|
||||
|
||||
interface LocationDisplayProps {
|
||||
interface LocationDisplayViewProps {
|
||||
location?: Location;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LocationDisplay = ({ location, className }: LocationDisplayProps) => {
|
||||
const LocationDisplayView = ({ location, className }: LocationDisplayViewProps) => {
|
||||
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
if (!location) {
|
||||
|
|
@ -49,4 +49,4 @@ const LocationDisplay = ({ location, className }: LocationDisplayProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default LocationDisplay;
|
||||
export default LocationDisplayView;
|
||||
3
web/src/components/MemoMetadata/Location/index.ts
Normal file
3
web/src/components/MemoMetadata/Location/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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";
|
|||
import { VisuallyHidden } from "@/components/ui/visually-hidden";
|
||||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
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 = ({
|
||||
open,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import MemoSnippetLink from "@/components/MemoView/components/MemoSnippetLink";
|
||||
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import MemoSnippetLink from "../MemoSnippetLink";
|
||||
|
||||
interface RelationCardProps {
|
||||
memo: MemoRelation_Memo;
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
import { LinkIcon, XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import RelationCard from "@/components/MemoView/components/metadata/RelationCard";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { memoServiceClient } from "@/connect";
|
||||
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 SectionHeader from "../SectionHeader";
|
||||
import RelationCard from "./RelationCard";
|
||||
|
||||
interface RelationListProps {
|
||||
interface RelationListEditorProps {
|
||||
relations: MemoRelation[];
|
||||
onRelationsChange?: (relations: MemoRelation[]) => void;
|
||||
parentPage?: string;
|
||||
|
|
@ -38,9 +39,10 @@ const RelationItemCard: FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
|
||||
const referenceRelations = relations.filter(
|
||||
(r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName),
|
||||
const RelationListEditor: FC<RelationListEditorProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
|
||||
const referenceRelations = useMemo(
|
||||
() => 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>>({});
|
||||
|
||||
|
|
@ -76,10 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
|
|||
|
||||
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 border-b border-border bg-muted/30">
|
||||
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
|
||||
</div>
|
||||
<SectionHeader icon={LinkIcon} title="Relations" count={referenceRelations.length} />
|
||||
|
||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||
{referenceRelations.map((relation) => {
|
||||
|
|
@ -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";
|
|||
import type { MemoRelation } 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 SectionHeader from "../SectionHeader";
|
||||
import RelationCard from "./RelationCard";
|
||||
import SectionHeader from "./SectionHeader";
|
||||
|
||||
interface RelationListProps {
|
||||
interface RelationListViewProps {
|
||||
relations: MemoRelation[];
|
||||
currentMemoName?: string;
|
||||
parentPage?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function RelationList({ relations, currentMemoName, parentPage, className }: RelationListProps) {
|
||||
function RelationListView({ relations, currentMemoName, parentPage, className }: RelationListViewProps) {
|
||||
const t = useTranslate();
|
||||
const [activeTab, setActiveTab] = useState<"referencing" | "referenced">("referencing");
|
||||
|
||||
|
|
@ -81,4 +81,4 @@ function RelationList({ relations, currentMemoName, parentPage, className }: Rel
|
|||
);
|
||||
}
|
||||
|
||||
export default RelationList;
|
||||
export default RelationListView;
|
||||
4
web/src/components/MemoMetadata/Relation/index.ts
Normal file
4
web/src/components/MemoMetadata/Relation/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { LinkMemoDialog } from "./LinkMemoDialog";
|
||||
export { default as RelationCard } from "./RelationCard";
|
||||
export { default as RelationListEditor } from "./RelationListEditor";
|
||||
export { default as RelationListView } from "./RelationListView";
|
||||
6
web/src/components/MemoMetadata/index.ts
Normal file
6
web/src/components/MemoMetadata/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// 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";
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { AttachmentListView, LocationDisplayView, RelationListView } from "@/components/MemoMetadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
|
@ -6,7 +7,6 @@ import { MemoReactionListView } from "../../MemoReactionListView";
|
|||
import { useMemoHandlers } from "../hooks";
|
||||
import { useMemoViewContext } from "../MemoViewContext";
|
||||
import type { MemoBodyProps } from "../types";
|
||||
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
|
||||
|
||||
const NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {
|
||||
const t = useTranslate();
|
||||
|
|
@ -44,9 +44,9 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
|
|||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
compact={memo.pinned ? false : compact} // Always show full content when pinned
|
||||
/>
|
||||
<AttachmentList attachments={memo.attachments} />
|
||||
<RelationList relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
|
||||
{memo.location && <LocationDisplay location={memo.location} />}
|
||||
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
|
||||
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
|
||||
{memo.location && <LocationDisplayView location={memo.location} />}
|
||||
<MemoReactionListView memo={memo} reactions={memo.reactions} />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
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";
|
|||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useParams } from "react-router-dom";
|
||||
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 { useSharedMemo } from "@/hooks/useMemoShareQueries";
|
||||
import { useUser } from "@/hooks/useUserQueries";
|
||||
|
|
@ -22,6 +24,7 @@ function withShareAttachmentLinks(attachments: Attachment[], token: string): Att
|
|||
const SharedMemo = () => {
|
||||
const t = useTranslate();
|
||||
const { token = "" } = useParams<{ token: string }>();
|
||||
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
|
||||
|
||||
const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token });
|
||||
const { data: creator } = useUser(memo?.creator ?? "", { enabled: !!memo?.creator });
|
||||
|
|
@ -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">
|
||||
<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>
|
||||
|
||||
<PreviewImageDialog
|
||||
open={previewState.open}
|
||||
onOpenChange={setPreviewOpen}
|
||||
imgUrls={previewState.urls}
|
||||
initialIndex={previewState.index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue