refactor: consolidate and update type definitions across MemoEditor components
This commit is contained in:
parent
40585607f4
commit
792d58b74d
17 changed files with 146 additions and 186 deletions
|
|
@ -1,14 +1,7 @@
|
||||||
import type { EditorRefActions } from ".";
|
import type { SlashCommandsProps } from "../types";
|
||||||
import type { Command } from "./commands";
|
|
||||||
import { SuggestionsPopup } from "./SuggestionsPopup";
|
import { SuggestionsPopup } from "./SuggestionsPopup";
|
||||||
import { useSuggestions } from "./useSuggestions";
|
import { useSuggestions } from "./useSuggestions";
|
||||||
|
|
||||||
interface SlashCommandsProps {
|
|
||||||
editorRef: React.RefObject<HTMLTextAreaElement>;
|
|
||||||
editorActions: React.ForwardedRef<EditorRefActions>;
|
|
||||||
commands: Command[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const SlashCommands = ({ editorRef, editorActions, commands }: SlashCommandsProps) => {
|
const SlashCommands = ({ editorRef, editorActions, commands }: SlashCommandsProps) => {
|
||||||
const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({
|
const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({
|
||||||
editorRef,
|
editorRef,
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,10 @@ import { matchPath } from "react-router-dom";
|
||||||
import OverflowTip from "@/components/kit/OverflowTip";
|
import OverflowTip from "@/components/kit/OverflowTip";
|
||||||
import { useTagCounts } from "@/hooks/useUserQueries";
|
import { useTagCounts } from "@/hooks/useUserQueries";
|
||||||
import { Routes } from "@/router";
|
import { Routes } from "@/router";
|
||||||
import type { EditorRefActions } from ".";
|
import type { TagSuggestionsProps } from "../types";
|
||||||
import { SuggestionsPopup } from "./SuggestionsPopup";
|
import { SuggestionsPopup } from "./SuggestionsPopup";
|
||||||
import { useSuggestions } from "./useSuggestions";
|
import { useSuggestions } from "./useSuggestions";
|
||||||
|
|
||||||
interface TagSuggestionsProps {
|
|
||||||
editorRef: React.RefObject<HTMLTextAreaElement>;
|
|
||||||
editorActions: React.ForwardedRef<EditorRefActions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TagSuggestions({ editorRef, editorActions }: TagSuggestionsProps) {
|
export default function TagSuggestions({ editorRef, editorActions }: TagSuggestionsProps) {
|
||||||
// On explore page, show all users' tags; otherwise show current user's tags
|
// On explore page, show all users' tags; otherwise show current user's tags
|
||||||
const isExplorePage = Boolean(matchPath(Routes.EXPLORE, window.location.pathname));
|
const isExplorePage = Boolean(matchPath(Routes.EXPLORE, window.location.pathname));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EDITOR_HEIGHT } from "../constants";
|
import { EDITOR_HEIGHT } from "../constants";
|
||||||
|
import type { EditorProps } from "../types";
|
||||||
import { editorCommands } from "./commands";
|
import { editorCommands } from "./commands";
|
||||||
import SlashCommands from "./SlashCommands";
|
import SlashCommands from "./SlashCommands";
|
||||||
import TagSuggestions from "./TagSuggestions";
|
import TagSuggestions from "./TagSuggestions";
|
||||||
|
|
@ -22,19 +23,7 @@ export interface EditorRefActions {
|
||||||
setLine: (lineNumber: number, text: string) => void;
|
setLine: (lineNumber: number, text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
const Editor = forwardRef(function Editor(props: EditorProps, ref: React.ForwardedRef<EditorRefActions>) {
|
||||||
className: string;
|
|
||||||
initialContent: string;
|
|
||||||
placeholder: string;
|
|
||||||
onContentChange: (content: string) => void;
|
|
||||||
onPaste: (event: React.ClipboardEvent) => void;
|
|
||||||
isFocusMode?: boolean;
|
|
||||||
isInIME?: boolean;
|
|
||||||
onCompositionStart?: () => void;
|
|
||||||
onCompositionEnd?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<EditorRefActions>) {
|
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
initialContent,
|
initialContent,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import { uniqBy } from "lodash-es";
|
import { uniqBy } from "lodash-es";
|
||||||
import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
|
import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import type { LocalFile } from "@/components/memo-metadata";
|
import type { LocalFile } from "@/components/memo-metadata";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,24 +14,17 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
useDropdownMenuSubHoverDelay,
|
useDropdownMenuSubHoverDelay,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import type { Location, 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 { LinkMemoDialog, LocationDialog } from "../components";
|
||||||
import { GEOCODING } from "../constants";
|
import { GEOCODING } from "../constants";
|
||||||
import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
|
import { useAbortController, useFileUpload, useLinkMemo, useLocation } from "../hooks";
|
||||||
import { useAbortController } from "../hooks/useAbortController";
|
import { useEditorContext } from "../state";
|
||||||
import { MemoEditorContext } from "../types";
|
import type { InsertMenuProps } from "../types";
|
||||||
|
|
||||||
interface Props {
|
const InsertMenu = (props: InsertMenuProps) => {
|
||||||
isUploading?: boolean;
|
|
||||||
location?: Location;
|
|
||||||
onLocationChange: (location?: Location) => void;
|
|
||||||
onToggleFocusMode?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InsertMenu = (props: Props) => {
|
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const context = useContext(MemoEditorContext);
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
|
|
||||||
const [linkDialogOpen, setLinkDialogOpen] = useState(false);
|
const [linkDialogOpen, setLinkDialogOpen] = useState(false);
|
||||||
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
|
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
|
||||||
|
|
@ -46,17 +39,15 @@ const InsertMenu = (props: Props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => {
|
const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => {
|
||||||
if (context.addLocalFiles) {
|
newFiles.forEach((file) => dispatch(actions.addLocalFile(file)));
|
||||||
context.addLocalFiles(newFiles);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkMemo = useLinkMemo({
|
const linkMemo = useLinkMemo({
|
||||||
isOpen: linkDialogOpen,
|
isOpen: linkDialogOpen,
|
||||||
currentMemoName: context.memoName,
|
currentMemoName: props.memoName,
|
||||||
existingRelations: context.relationList,
|
existingRelations: state.metadata.relations,
|
||||||
onAddRelation: (relation: MemoRelation) => {
|
onAddRelation: (relation: MemoRelation) => {
|
||||||
context.setRelationList(uniqBy([...context.relationList, relation], (r) => r.relatedMemo?.name));
|
dispatch(actions.setMetadata({ relations: uniqBy([...state.metadata.relations, relation], (r) => r.relatedMemo?.name) }));
|
||||||
setLinkDialogOpen(false);
|
setLinkDialogOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
||||||
import VisibilityIcon from "@/components/VisibilityIcon";
|
import VisibilityIcon from "@/components/VisibilityIcon";
|
||||||
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
import type { VisibilitySelectorProps } from "../types";
|
||||||
|
|
||||||
interface Props {
|
const VisibilitySelector = (props: VisibilitySelectorProps) => {
|
||||||
value: Visibility;
|
|
||||||
onChange: (visibility: Visibility) => void;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VisibilitySelector = (props: Props) => {
|
|
||||||
const { value, onChange } = props;
|
const { value, onChange } = props;
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@ import type { LocalFile } from "@/components/memo-metadata";
|
||||||
import Editor, { type EditorRefActions } from "../Editor";
|
import Editor, { type EditorRefActions } from "../Editor";
|
||||||
import { useBlobUrls, useDragAndDrop } from "../hooks";
|
import { useBlobUrls, useDragAndDrop } from "../hooks";
|
||||||
import { useEditorContext } from "../state";
|
import { useEditorContext } from "../state";
|
||||||
|
import type { EditorContentProps } from "../types";
|
||||||
interface EditorContentProps {
|
|
||||||
placeholder?: string;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {
|
export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {
|
||||||
const { state, actions, dispatch } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { AttachmentList, LocationDisplay, RelationList } from "@/components/memo-metadata";
|
import { AttachmentList, LocationDisplay, RelationList } from "@/components/memo-metadata";
|
||||||
import { useEditorContext } from "../state";
|
import { useEditorContext } from "../state";
|
||||||
|
import type { EditorMetadataProps } from "../types";
|
||||||
|
|
||||||
export const EditorMetadata: FC = () => {
|
export const EditorMetadata: FC<EditorMetadataProps> = () => {
|
||||||
const { state, actions, dispatch } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,9 @@ import { validationService } from "../services";
|
||||||
import { useEditorContext } from "../state";
|
import { useEditorContext } from "../state";
|
||||||
import InsertMenu from "../Toolbar/InsertMenu";
|
import InsertMenu from "../Toolbar/InsertMenu";
|
||||||
import VisibilitySelector from "../Toolbar/VisibilitySelector";
|
import VisibilitySelector from "../Toolbar/VisibilitySelector";
|
||||||
|
import type { EditorToolbarProps } from "../types";
|
||||||
|
|
||||||
interface EditorToolbarProps {
|
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel, memoName }) => {
|
||||||
onSave: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel }) => {
|
|
||||||
const { state, actions, dispatch } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
const { valid } = validationService.canSave(state);
|
const { valid } = validationService.canSave(state);
|
||||||
|
|
||||||
|
|
@ -36,6 +32,7 @@ export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel }) => {
|
||||||
location={state.metadata.location}
|
location={state.metadata.location}
|
||||||
onLocationChange={handleLocationChange}
|
onLocationChange={handleLocationChange}
|
||||||
onToggleFocusMode={handleToggleFocusMode}
|
onToggleFocusMode={handleToggleFocusMode}
|
||||||
|
memoName={memoName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import { Minimize2Icon } from "lucide-react";
|
import { Minimize2Icon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { FOCUS_MODE_STYLES } from "../constants";
|
import { FOCUS_MODE_STYLES } from "../constants";
|
||||||
|
import type { FocusModeExitButtonProps, FocusModeOverlayProps } from "../types";
|
||||||
interface FocusModeOverlayProps {
|
|
||||||
isActive: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) {
|
export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) {
|
||||||
if (!isActive) return null;
|
if (!isActive) return null;
|
||||||
|
|
@ -21,12 +17,6 @@ export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FocusModeExitButtonProps {
|
|
||||||
isActive: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FocusModeExitButton({ isActive, onToggle, title }: FocusModeExitButtonProps) {
|
export function FocusModeExitButton({ isActive, onToggle, title }: FocusModeExitButtonProps) {
|
||||||
if (!isActive) return null;
|
if (!isActive) return null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
import type { LinkMemoDialogProps } from "../types";
|
||||||
|
|
||||||
function highlightSearchText(content: string, searchText: string): React.ReactNode {
|
function highlightSearchText(content: string, searchText: string): React.ReactNode {
|
||||||
if (!searchText) return content;
|
if (!searchText) return content;
|
||||||
|
|
@ -29,16 +29,6 @@ function highlightSearchText(content: string, searchText: string): React.ReactNo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkMemoDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
searchText: string;
|
|
||||||
onSearchChange: (text: string) => void;
|
|
||||||
filteredMemos: Memo[];
|
|
||||||
isFetching: boolean;
|
|
||||||
onSelectMemo: (memo: Memo) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkMemoDialog = ({
|
export const LinkMemoDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { LatLng } from "leaflet";
|
|
||||||
import LeafletMap from "@/components/LeafletMap";
|
import LeafletMap from "@/components/LeafletMap";
|
||||||
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";
|
||||||
|
|
@ -7,19 +6,7 @@ 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 { LocationState } from "../types/insert-menu";
|
import type { LocationDialogProps } from "../types";
|
||||||
|
|
||||||
interface LocationDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
state: LocationState;
|
|
||||||
locationInitialized: boolean;
|
|
||||||
onPositionChange: (position: LatLng) => void;
|
|
||||||
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
|
|
||||||
onPlaceholderChange: (value: string) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LocationDialog = ({
|
export const LocationDialog = ({
|
||||||
open,
|
open,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,5 @@ export { useFileUpload } from "./useFileUpload";
|
||||||
export { useFocusMode } from "./useFocusMode";
|
export { useFocusMode } from "./useFocusMode";
|
||||||
export { useKeyboard } from "./useKeyboard";
|
export { useKeyboard } from "./useKeyboard";
|
||||||
export { useLinkMemo } from "./useLinkMemo";
|
export { useLinkMemo } from "./useLinkMemo";
|
||||||
export { useLocalFileManager } from "./useLocalFileManager";
|
|
||||||
export { useLocation } from "./useLocation";
|
export { useLocation } from "./useLocation";
|
||||||
export { useMemoInit } from "./useMemoInit";
|
export { useMemoInit } from "./useMemoInit";
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function useDragAndDrop(onDrop: (files: FileList) => void) {
|
export function useDragAndDrop(onDrop: (files: FileList) => void) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDragging,
|
|
||||||
dragHandlers: {
|
dragHandlers: {
|
||||||
onDragOver: (e: React.DragEvent) => {
|
onDragOver: (e: React.DragEvent) => {
|
||||||
if (e.dataTransfer?.types.includes("Files")) {
|
if (e.dataTransfer?.types.includes("Files")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = "copy";
|
e.dataTransfer.dropEffect = "copy";
|
||||||
setIsDragging(true);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragLeave: (e: React.DragEvent) => {
|
onDragLeave: (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(false);
|
|
||||||
},
|
},
|
||||||
onDrop: (e: React.DragEvent) => {
|
onDrop: (e: React.DragEvent) => {
|
||||||
if (e.dataTransfer?.files.length) {
|
if (e.dataTransfer?.files.length) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(false);
|
|
||||||
onDrop(e.dataTransfer.files);
|
onDrop(e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import type { LocalFile } from "@/components/memo-metadata";
|
|
||||||
import { useBlobUrls } from "./useBlobUrls";
|
|
||||||
|
|
||||||
export function useLocalFileManager() {
|
|
||||||
const [localFiles, setLocalFiles] = useState<LocalFile[]>([]);
|
|
||||||
const { createBlobUrl, revokeBlobUrl } = useBlobUrls();
|
|
||||||
|
|
||||||
const addFiles = (files: FileList | File[]): void => {
|
|
||||||
const fileArray = Array.from(files);
|
|
||||||
const newLocalFiles: LocalFile[] = fileArray.map((file) => ({
|
|
||||||
file,
|
|
||||||
previewUrl: createBlobUrl(file),
|
|
||||||
}));
|
|
||||||
setLocalFiles((prev) => [...prev, ...newLocalFiles]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFile = (previewUrl: string): void => {
|
|
||||||
setLocalFiles((prev) => {
|
|
||||||
const toRemove = prev.find((f) => f.previewUrl === previewUrl);
|
|
||||||
if (toRemove) {
|
|
||||||
revokeBlobUrl(toRemove.previewUrl);
|
|
||||||
}
|
|
||||||
return prev.filter((f) => f.previewUrl !== previewUrl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFiles = (): void => {
|
|
||||||
localFiles.forEach(({ previewUrl }) => revokeBlobUrl(previewUrl));
|
|
||||||
setLocalFiles([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
localFiles,
|
|
||||||
addFiles,
|
|
||||||
removeFile,
|
|
||||||
clearFiles,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { memoKeys } from "@/hooks/useMemoQueries";
|
import { memoKeys } from "@/hooks/useMemoQueries";
|
||||||
|
|
@ -13,18 +13,7 @@ import type { EditorRefActions } from "./Editor";
|
||||||
import { useAutoSave, useFocusMode, useKeyboard, useMemoInit } from "./hooks";
|
import { useAutoSave, useFocusMode, useKeyboard, useMemoInit } from "./hooks";
|
||||||
import { cacheService, errorService, memoService, validationService } from "./services";
|
import { cacheService, errorService, memoService, validationService } from "./services";
|
||||||
import { EditorProvider, useEditorContext } from "./state";
|
import { EditorProvider, useEditorContext } from "./state";
|
||||||
import { MemoEditorContext } from "./types";
|
import type { MemoEditorProps } from "./types";
|
||||||
|
|
||||||
export interface MemoEditorProps {
|
|
||||||
className?: string;
|
|
||||||
cacheKey?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
memoName?: string;
|
|
||||||
parentMemoName?: string;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
onConfirm?: (memoName: string) => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MemoEditor = (props: MemoEditorProps) => {
|
const MemoEditor = (props: MemoEditorProps) => {
|
||||||
const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props;
|
const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props;
|
||||||
|
|
@ -61,26 +50,6 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
||||||
const editorRef = useRef<EditorRefActions>(null);
|
const editorRef = useRef<EditorRefActions>(null);
|
||||||
const { state, actions, dispatch } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
|
|
||||||
// Bridge for old MemoEditorContext (used by InsertMenu and other components)
|
|
||||||
const legacyContextValue = useMemo(
|
|
||||||
() => ({
|
|
||||||
attachmentList: state.metadata.attachments,
|
|
||||||
relationList: state.metadata.relations,
|
|
||||||
setAttachmentList: (attachments: typeof state.metadata.attachments) => dispatch(actions.setMetadata({ attachments })),
|
|
||||||
setRelationList: (relations: typeof state.metadata.relations) => dispatch(actions.setMetadata({ relations })),
|
|
||||||
memoName,
|
|
||||||
addLocalFiles: (files: typeof state.localFiles) => {
|
|
||||||
files.forEach((file) => {
|
|
||||||
dispatch(actions.addLocalFile(file));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeLocalFile: (previewUrl: string) => dispatch(actions.removeLocalFile(previewUrl)),
|
|
||||||
localFiles: state.localFiles,
|
|
||||||
}),
|
|
||||||
[state.metadata.attachments, state.metadata.relations, state.localFiles, memoName, actions, dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize editor (load memo or cache)
|
|
||||||
useMemoInit(editorRef, memoName, cacheKey, currentUser?.name ?? "", autoFocus);
|
useMemoInit(editorRef, memoName, cacheKey, currentUser?.name ?? "", autoFocus);
|
||||||
|
|
||||||
// Auto-save content to localStorage
|
// Auto-save content to localStorage
|
||||||
|
|
@ -149,7 +118,7 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoEditorContext.Provider value={legacyContextValue}>
|
<>
|
||||||
<FocusModeOverlay isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} />
|
<FocusModeOverlay isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|
@ -175,10 +144,10 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
||||||
{/* Metadata and toolbar grouped together at bottom */}
|
{/* Metadata and toolbar grouped together at bottom */}
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<EditorMetadata />
|
<EditorMetadata />
|
||||||
<EditorToolbar onSave={handleSave} onCancel={onCancel} />
|
<EditorToolbar onSave={handleSave} onCancel={onCancel} memoName={memoName} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MemoEditorContext.Provider>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
99
web/src/components/MemoEditor/types/components.ts
Normal file
99
web/src/components/MemoEditor/types/components.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
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;
|
||||||
|
cacheKey?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
memoName?: string;
|
||||||
|
parentMemoName?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
onConfirm?: (memoName: string) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorContentProps {
|
||||||
|
placeholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorToolbarProps {
|
||||||
|
onSave: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
memoName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorMetadataProps {}
|
||||||
|
|
||||||
|
export interface FocusModeOverlayProps {
|
||||||
|
isActive: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FocusModeExitButtonProps {
|
||||||
|
isActive: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
state: LocationState;
|
||||||
|
locationInitialized: boolean;
|
||||||
|
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;
|
||||||
|
onLocationChange: (location?: Location) => void;
|
||||||
|
onToggleFocusMode?: () => void;
|
||||||
|
memoName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagSuggestionsProps {
|
||||||
|
editorRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
editorActions: React.ForwardedRef<EditorRefActions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlashCommandsProps {
|
||||||
|
editorRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
editorActions: React.ForwardedRef<EditorRefActions>;
|
||||||
|
commands: Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorProps {
|
||||||
|
className: string;
|
||||||
|
initialContent: string;
|
||||||
|
placeholder: string;
|
||||||
|
onContentChange: (content: string) => void;
|
||||||
|
onPaste: (event: React.ClipboardEvent) => void;
|
||||||
|
isFocusMode?: boolean;
|
||||||
|
isInIME?: boolean;
|
||||||
|
onCompositionStart?: () => void;
|
||||||
|
onCompositionEnd?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisibilitySelectorProps {
|
||||||
|
value: Visibility;
|
||||||
|
onChange: (visibility: Visibility) => void;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
// MemoEditor type exports
|
// MemoEditor type exports
|
||||||
|
|
||||||
|
export type {
|
||||||
|
EditorContentProps,
|
||||||
|
EditorMetadataProps,
|
||||||
|
EditorProps,
|
||||||
|
EditorToolbarProps,
|
||||||
|
FocusModeExitButtonProps,
|
||||||
|
FocusModeOverlayProps,
|
||||||
|
InsertMenuProps,
|
||||||
|
LinkMemoDialogProps,
|
||||||
|
LocationDialogProps,
|
||||||
|
MemoEditorProps,
|
||||||
|
SlashCommandsProps,
|
||||||
|
TagSuggestionsProps,
|
||||||
|
VisibilitySelectorProps,
|
||||||
|
} from "./components";
|
||||||
export { MemoEditorContext, type MemoEditorContextValue } from "./context";
|
export { MemoEditorContext, type MemoEditorContextValue } from "./context";
|
||||||
export type { LocationState } from "./insert-menu";
|
export type { LocationState } from "./insert-menu";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue