mirror of https://github.com/usememos/memos.git
refactor: streamline tag sorting and update coordinate handling in MemoEditor components
This commit is contained in:
parent
d537591005
commit
8a7c976758
|
|
@ -13,11 +13,9 @@ interface TagSuggestionsProps {
|
||||||
|
|
||||||
const TagSuggestions = observer(({ editorRef, editorActions }: TagSuggestionsProps) => {
|
const TagSuggestions = observer(({ editorRef, editorActions }: TagSuggestionsProps) => {
|
||||||
const sortedTags = useMemo(() => {
|
const sortedTags = useMemo(() => {
|
||||||
const tags = Object.entries(userStore.state.tagCount)
|
return Object.entries(userStore.state.tagCount)
|
||||||
.sort((a, b) => b[1] - a[1]) // Sort by usage count (descending)
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||||
.map(([tag]) => tag);
|
.map(([tag]) => tag);
|
||||||
// Secondary sort by name for stable ordering
|
|
||||||
return tags.sort((a, b) => (userStore.state.tagCount[a] === userStore.state.tagCount[b] ? a.localeCompare(b) : 0));
|
|
||||||
}, [userStore.state.tagCount]);
|
}, [userStore.state.tagCount]);
|
||||||
|
|
||||||
const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({
|
const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import { useListCompletion } from "./useListCompletion";
|
||||||
|
|
||||||
export interface EditorRefActions {
|
export interface EditorRefActions {
|
||||||
getEditor: () => HTMLTextAreaElement | null;
|
getEditor: () => HTMLTextAreaElement | null;
|
||||||
focus: FunctionType;
|
focus: () => void;
|
||||||
scrollToCursor: FunctionType;
|
scrollToCursor: () => void;
|
||||||
insertText: (text: string, prefix?: string, suffix?: string) => void;
|
insertText: (text: string, prefix?: string, suffix?: string) => void;
|
||||||
removeText: (start: number, length: number) => void;
|
removeText: (start: number, length: number) => void;
|
||||||
setContent: (text: string) => void;
|
setContent: (text: string) => void;
|
||||||
|
|
|
||||||
|
|
@ -214,8 +214,7 @@ const InsertMenu = observer((props: Props) => {
|
||||||
state={location.state}
|
state={location.state}
|
||||||
locationInitialized={location.locationInitialized}
|
locationInitialized={location.locationInitialized}
|
||||||
onPositionChange={handlePositionChange}
|
onPositionChange={handlePositionChange}
|
||||||
onLatChange={location.handleLatChange}
|
onUpdateCoordinate={location.updateCoordinate}
|
||||||
onLngChange={location.handleLngChange}
|
|
||||||
onPlaceholderChange={location.setPlaceholder}
|
onPlaceholderChange={location.setPlaceholder}
|
||||||
onCancel={handleLocationCancel}
|
onCancel={handleLocationCancel}
|
||||||
onConfirm={handleLocationConfirm}
|
onConfirm={handleLocationConfirm}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const VisibilitySelector = (props: Props) => {
|
||||||
{ value: Visibility.PRIVATE, label: t("memo.visibility.private") },
|
{ value: Visibility.PRIVATE, label: t("memo.visibility.private") },
|
||||||
{ value: Visibility.PROTECTED, label: t("memo.visibility.protected") },
|
{ value: Visibility.PROTECTED, label: t("memo.visibility.protected") },
|
||||||
{ value: Visibility.PUBLIC, label: t("memo.visibility.public") },
|
{ value: Visibility.PUBLIC, label: t("memo.visibility.public") },
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || "";
|
const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
fallback?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasError: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MemoEditorErrorBoundary extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error): State {
|
|
||||||
// Update state so the next render will show the fallback UI
|
|
||||||
return { hasError: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
||||||
// Log the error to console for debugging
|
|
||||||
console.error("MemoEditor Error:", error, errorInfo);
|
|
||||||
// You can also log the error to an error reporting service here
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReset = () => {
|
|
||||||
this.setState({ hasError: false, error: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
// Custom fallback UI
|
|
||||||
if (this.props.fallback) {
|
|
||||||
return this.props.fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback UI
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col justify-center items-center bg-card px-4 py-8 rounded-lg border border-destructive/50">
|
|
||||||
<AlertCircle className="w-8 h-8 text-destructive mb-3" />
|
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-2">Editor Error</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4 text-center max-w-md">
|
|
||||||
Something went wrong with the memo editor. Please try refreshing the page.
|
|
||||||
</p>
|
|
||||||
{this.state.error && (
|
|
||||||
<details className="text-xs text-muted-foreground mb-4 max-w-md">
|
|
||||||
<summary className="cursor-pointer hover:text-foreground">Error details</summary>
|
|
||||||
<pre className="mt-2 p-2 bg-muted rounded text-xs overflow-x-auto">{this.state.error.toString()}</pre>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={this.handleReset}
|
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MemoEditorErrorBoundary;
|
|
||||||
|
|
@ -15,8 +15,7 @@ interface LocationDialogProps {
|
||||||
state: LocationState;
|
state: LocationState;
|
||||||
locationInitialized: boolean;
|
locationInitialized: boolean;
|
||||||
onPositionChange: (position: LatLng) => void;
|
onPositionChange: (position: LatLng) => void;
|
||||||
onLatChange: (value: string) => void;
|
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
|
||||||
onLngChange: (value: string) => void;
|
|
||||||
onPlaceholderChange: (value: string) => void;
|
onPlaceholderChange: (value: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
|
|
@ -28,8 +27,7 @@ export const LocationDialog = ({
|
||||||
state,
|
state,
|
||||||
locationInitialized,
|
locationInitialized,
|
||||||
onPositionChange,
|
onPositionChange,
|
||||||
onLatChange,
|
onUpdateCoordinate,
|
||||||
onLngChange,
|
|
||||||
onPlaceholderChange,
|
onPlaceholderChange,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
|
@ -67,7 +65,7 @@ export const LocationDialog = ({
|
||||||
min="-90"
|
min="-90"
|
||||||
max="90"
|
max="90"
|
||||||
value={latInput}
|
value={latInput}
|
||||||
onChange={(e) => onLatChange(e.target.value)}
|
onChange={(e) => onUpdateCoordinate("lat", e.target.value)}
|
||||||
className="h-9"
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,7 +81,7 @@ export const LocationDialog = ({
|
||||||
min="-180"
|
min="-180"
|
||||||
max="180"
|
max="180"
|
||||||
value={lngInput}
|
value={lngInput}
|
||||||
onChange={(e) => onLngChange(e.target.value)}
|
onChange={(e) => onUpdateCoordinate("lng", e.target.value)}
|
||||||
className="h-9"
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// UI components for MemoEditor
|
// UI components for MemoEditor
|
||||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
|
||||||
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
|
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
|
||||||
export { LinkMemoDialog } from "./LinkMemoDialog";
|
export { LinkMemoDialog } from "./LinkMemoDialog";
|
||||||
export { LocationDialog } from "./LocationDialog";
|
export { LocationDialog } from "./LocationDialog";
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,16 @@ export const useLocation = (initialLocation?: Location) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePositionChange = (position: LatLng) => {
|
const handlePositionChange = (position: LatLng) => {
|
||||||
if (!locationInitialized) {
|
if (!locationInitialized) setLocationInitialized(true);
|
||||||
setLocationInitialized(true);
|
|
||||||
}
|
|
||||||
updatePosition(position);
|
updatePosition(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLatChange = (value: string) => {
|
const updateCoordinate = (type: "lat" | "lng", value: string) => {
|
||||||
setState((prev) => ({ ...prev, latInput: value }));
|
setState((prev) => ({ ...prev, [type === "lat" ? "latInput" : "lngInput"]: value }));
|
||||||
const lat = parseFloat(value);
|
const num = parseFloat(value);
|
||||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 && state.position) {
|
const isValid = type === "lat" ? !isNaN(num) && num >= -90 && num <= 90 : !isNaN(num) && num >= -180 && num <= 180;
|
||||||
updatePosition(new LatLng(lat, state.position.lng));
|
if (isValid && state.position) {
|
||||||
}
|
updatePosition(type === "lat" ? new LatLng(num, state.position.lng) : new LatLng(state.position.lat, num));
|
||||||
};
|
|
||||||
|
|
||||||
const handleLngChange = (value: string) => {
|
|
||||||
setState((prev) => ({ ...prev, lngInput: value }));
|
|
||||||
const lng = parseFloat(value);
|
|
||||||
if (!isNaN(lng) && lng >= -180 && lng <= 180 && state.position) {
|
|
||||||
updatePosition(new LatLng(state.position.lat, lng));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,8 +65,7 @@ export const useLocation = (initialLocation?: Location) => {
|
||||||
state,
|
state,
|
||||||
locationInitialized,
|
locationInitialized,
|
||||||
handlePositionChange,
|
handlePositionChange,
|
||||||
handleLatChange,
|
updateCoordinate,
|
||||||
handleLngChange,
|
|
||||||
setPlaceholder,
|
setPlaceholder,
|
||||||
reset,
|
reset,
|
||||||
getLocation,
|
getLocation,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import DateTimeInput from "../DateTimeInput";
|
import DateTimeInput from "../DateTimeInput";
|
||||||
import { AttachmentList, LocationDisplay, RelationList } from "../memo-metadata";
|
import { AttachmentList, LocationDisplay, RelationList } from "../memo-metadata";
|
||||||
import { ErrorBoundary, FocusModeExitButton, FocusModeOverlay } from "./components";
|
import { FocusModeExitButton, FocusModeOverlay } from "./components";
|
||||||
import { FOCUS_MODE_STYLES, LOCALSTORAGE_DEBOUNCE_DELAY } from "./constants";
|
import { FOCUS_MODE_STYLES, LOCALSTORAGE_DEBOUNCE_DELAY } from "./constants";
|
||||||
import Editor, { type EditorRefActions } from "./Editor";
|
import Editor, { type EditorRefActions } from "./Editor";
|
||||||
import {
|
import {
|
||||||
|
|
@ -224,113 +224,111 @@ const MemoEditor = observer((props: Props) => {
|
||||||
const allowSave = (hasContent || attachmentList.length > 0 || localFiles.length > 0) && !isUploadingAttachment && !isRequesting;
|
const allowSave = (hasContent || attachmentList.length > 0 || localFiles.length > 0) && !isUploadingAttachment && !isRequesting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<MemoEditorContext.Provider
|
||||||
<MemoEditorContext.Provider
|
value={{
|
||||||
value={{
|
attachmentList,
|
||||||
attachmentList,
|
relationList,
|
||||||
relationList,
|
setAttachmentList,
|
||||||
setAttachmentList,
|
addLocalFiles: (files) => addFiles(Array.from(files.map((f) => f.file))),
|
||||||
addLocalFiles: (files) => addFiles(Array.from(files.map((f) => f.file))),
|
removeLocalFile: removeFile,
|
||||||
removeLocalFile: removeFile,
|
localFiles,
|
||||||
localFiles,
|
setRelationList,
|
||||||
setRelationList,
|
memoName,
|
||||||
memoName,
|
}}
|
||||||
}}
|
>
|
||||||
|
{/* Focus Mode Backdrop */}
|
||||||
|
<FocusModeOverlay isActive={isFocusMode} onToggle={toggleFocusMode} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative w-full flex flex-col justify-start items-start bg-card px-4 pt-3 pb-2 rounded-lg border",
|
||||||
|
FOCUS_MODE_STYLES.transition,
|
||||||
|
isDraggingFile ? "border-dashed border-muted-foreground cursor-copy" : "border-border cursor-auto",
|
||||||
|
isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
{...dragHandlers}
|
||||||
|
onFocus={handleEditorFocus}
|
||||||
>
|
>
|
||||||
{/* Focus Mode Backdrop */}
|
{/* Focus Mode Exit Button */}
|
||||||
<FocusModeOverlay isActive={isFocusMode} onToggle={toggleFocusMode} />
|
<FocusModeExitButton isActive={isFocusMode} onToggle={toggleFocusMode} title={t("editor.exit-focus-mode")} />
|
||||||
|
|
||||||
<div
|
<Editor ref={editorRef} {...editorConfig} />
|
||||||
className={cn(
|
<LocationDisplay mode="edit" location={location} onRemove={() => setLocation(undefined)} />
|
||||||
"group relative w-full flex flex-col justify-start items-start bg-card px-4 pt-3 pb-2 rounded-lg border",
|
{/* Show attachments and pending files together */}
|
||||||
FOCUS_MODE_STYLES.transition,
|
<AttachmentList
|
||||||
isDraggingFile ? "border-dashed border-muted-foreground cursor-copy" : "border-border cursor-auto",
|
mode="edit"
|
||||||
isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),
|
attachments={attachmentList}
|
||||||
className,
|
onAttachmentsChange={setAttachmentList}
|
||||||
)}
|
localFiles={localFiles}
|
||||||
tabIndex={0}
|
onRemoveLocalFile={removeFile}
|
||||||
onKeyDown={handleKeyDown}
|
/>
|
||||||
{...dragHandlers}
|
<RelationList mode="edit" relations={referenceRelations} onRelationsChange={setRelationList} />
|
||||||
onFocus={handleEditorFocus}
|
<div className="relative w-full flex flex-row justify-between items-center pt-2 gap-2" onFocus={(e) => e.stopPropagation()}>
|
||||||
>
|
<div className="flex flex-row justify-start items-center gap-1">
|
||||||
{/* Focus Mode Exit Button */}
|
<InsertMenu
|
||||||
<FocusModeExitButton isActive={isFocusMode} onToggle={toggleFocusMode} title={t("editor.exit-focus-mode")} />
|
isUploading={isUploadingAttachment}
|
||||||
|
location={location}
|
||||||
<Editor ref={editorRef} {...editorConfig} />
|
onLocationChange={setLocation}
|
||||||
<LocationDisplay mode="edit" location={location} onRemove={() => setLocation(undefined)} />
|
onToggleFocusMode={toggleFocusMode}
|
||||||
{/* Show attachments and pending files together */}
|
/>
|
||||||
<AttachmentList
|
</div>
|
||||||
mode="edit"
|
<div className="shrink-0 flex flex-row justify-end items-center">
|
||||||
attachments={attachmentList}
|
<VisibilitySelector value={memoVisibility} onChange={setMemoVisibility} />
|
||||||
onAttachmentsChange={setAttachmentList}
|
<div className="flex flex-row justify-end gap-1">
|
||||||
localFiles={localFiles}
|
{props.onCancel && (
|
||||||
onRemoveLocalFile={removeFile}
|
<Button
|
||||||
/>
|
variant="ghost"
|
||||||
<RelationList mode="edit" relations={referenceRelations} onRelationsChange={setRelationList} />
|
disabled={isRequesting}
|
||||||
<div className="relative w-full flex flex-row justify-between items-center pt-2 gap-2" onFocus={(e) => e.stopPropagation()}>
|
onClick={() => {
|
||||||
<div className="flex flex-row justify-start items-center gap-1">
|
clearFiles();
|
||||||
<InsertMenu
|
if (props.onCancel) props.onCancel();
|
||||||
isUploading={isUploadingAttachment}
|
}}
|
||||||
location={location}
|
>
|
||||||
onLocationChange={setLocation}
|
{t("common.cancel")}
|
||||||
onToggleFocusMode={toggleFocusMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 flex flex-row justify-end items-center">
|
|
||||||
<VisibilitySelector value={memoVisibility} onChange={setMemoVisibility} />
|
|
||||||
<div className="flex flex-row justify-end gap-1">
|
|
||||||
{props.onCancel && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
disabled={isRequesting}
|
|
||||||
onClick={() => {
|
|
||||||
clearFiles();
|
|
||||||
if (props.onCancel) props.onCancel();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button disabled={!allowSave || isRequesting} onClick={handleSaveBtnClick}>
|
|
||||||
{isRequesting ? <LoaderIcon className="w-4 h-4 animate-spin" /> : t("editor.save")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
|
<Button disabled={!allowSave || isRequesting} onClick={handleSaveBtnClick}>
|
||||||
|
{isRequesting ? <LoaderIcon className="w-4 h-4 animate-spin" /> : t("editor.save")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Show memo metadata if memoName is provided */}
|
{/* Show memo metadata if memoName is provided */}
|
||||||
{memoName && (
|
{memoName && (
|
||||||
<div className="w-full -mt-1 mb-4 text-xs leading-5 px-4 opacity-60 font-mono text-muted-foreground">
|
<div className="w-full -mt-1 mb-4 text-xs leading-5 px-4 opacity-60 font-mono text-muted-foreground">
|
||||||
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5 items-center">
|
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5 items-center">
|
||||||
{!isEqual(createTime, updateTime) && updateTime && (
|
{!isEqual(createTime, updateTime) && updateTime && (
|
||||||
<>
|
<>
|
||||||
<span className="text-left">Updated</span>
|
<span className="text-left">Updated</span>
|
||||||
<DateTimeInput value={updateTime} onChange={setUpdateTime} />
|
<DateTimeInput value={updateTime} onChange={setUpdateTime} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{createTime && (
|
{createTime && (
|
||||||
<>
|
<>
|
||||||
<span className="text-left">Created</span>
|
<span className="text-left">Created</span>
|
||||||
<DateTimeInput value={createTime} onChange={setCreateTime} />
|
<DateTimeInput value={createTime} onChange={setCreateTime} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span className="text-left">ID</span>
|
<span className="text-left">ID</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-1 border border-transparent cursor-default text-left"
|
className="px-1 border border-transparent cursor-default text-left"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy(extractMemoIdFromName(memoName));
|
copy(extractMemoIdFromName(memoName));
|
||||||
toast.success(t("message.copied"));
|
toast.success(t("message.copied"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{extractMemoIdFromName(memoName)}
|
{extractMemoIdFromName(memoName)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</MemoEditorContext.Provider>
|
)}
|
||||||
</ErrorBoundary>
|
</MemoEditorContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue