Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
8a7c9767
Commit
8a7c9767
authored
Dec 22, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: streamline tag sorting and update coordinate handling in MemoEditor components
parent
d5375910
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
116 additions
and
204 deletions
+116
-204
TagSuggestions.tsx
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
+2
-4
index.tsx
web/src/components/MemoEditor/Editor/index.tsx
+2
-2
InsertMenu.tsx
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
+1
-2
VisibilitySelector.tsx
web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx
+1
-1
ErrorBoundary.tsx
web/src/components/MemoEditor/components/ErrorBoundary.tsx
+0
-70
LocationDialog.tsx
web/src/components/MemoEditor/components/LocationDialog.tsx
+4
-6
index.ts
web/src/components/MemoEditor/components/index.ts
+0
-1
useLocation.ts
web/src/components/MemoEditor/hooks/useLocation.ts
+8
-18
index.tsx
web/src/components/MemoEditor/index.tsx
+98
-100
No files found.
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
View file @
8a7c9767
...
...
@@ -13,11 +13,9 @@ interface TagSuggestionsProps {
const
TagSuggestions
=
observer
(({
editorRef
,
editorActions
}:
TagSuggestionsProps
)
=>
{
const
sortedTags
=
useMemo
(()
=>
{
const
tags
=
Object
.
entries
(
userStore
.
state
.
tagCount
)
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
]
)
// Sort by usage count (descending
)
return
Object
.
entries
(
userStore
.
state
.
tagCount
)
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
]
||
a
[
0
].
localeCompare
(
b
[
0
])
)
.
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
]);
const
{
position
,
suggestions
,
selectedIndex
,
isVisible
,
handleItemSelect
}
=
useSuggestions
({
...
...
web/src/components/MemoEditor/Editor/index.tsx
View file @
8a7c9767
...
...
@@ -8,8 +8,8 @@ import { useListCompletion } from "./useListCompletion";
export
interface
EditorRefActions
{
getEditor
:
()
=>
HTMLTextAreaElement
|
null
;
focus
:
FunctionType
;
scrollToCursor
:
FunctionType
;
focus
:
()
=>
void
;
scrollToCursor
:
()
=>
void
;
insertText
:
(
text
:
string
,
prefix
?:
string
,
suffix
?:
string
)
=>
void
;
removeText
:
(
start
:
number
,
length
:
number
)
=>
void
;
setContent
:
(
text
:
string
)
=>
void
;
...
...
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
View file @
8a7c9767
...
...
@@ -214,8 +214,7 @@ const InsertMenu = observer((props: Props) => {
state=
{
location
.
state
}
locationInitialized=
{
location
.
locationInitialized
}
onPositionChange=
{
handlePositionChange
}
onLatChange=
{
location
.
handleLatChange
}
onLngChange=
{
location
.
handleLngChange
}
onUpdateCoordinate=
{
location
.
updateCoordinate
}
onPlaceholderChange=
{
location
.
setPlaceholder
}
onCancel=
{
handleLocationCancel
}
onConfirm=
{
handleLocationConfirm
}
...
...
web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx
View file @
8a7c9767
...
...
@@ -18,7 +18,7 @@ const VisibilitySelector = (props: Props) => {
{
value
:
Visibility
.
PRIVATE
,
label
:
t
(
"memo.visibility.private"
)
},
{
value
:
Visibility
.
PROTECTED
,
label
:
t
(
"memo.visibility.protected"
)
},
{
value
:
Visibility
.
PUBLIC
,
label
:
t
(
"memo.visibility.public"
)
},
];
]
as
const
;
const
currentLabel
=
visibilityOptions
.
find
((
option
)
=>
option
.
value
===
value
)?.
label
||
""
;
...
...
web/src/components/MemoEditor/components/ErrorBoundary.tsx
deleted
100644 → 0
View file @
d5375910
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
;
web/src/components/MemoEditor/components/LocationDialog.tsx
View file @
8a7c9767
...
...
@@ -15,8 +15,7 @@ interface LocationDialogProps {
state
:
LocationState
;
locationInitialized
:
boolean
;
onPositionChange
:
(
position
:
LatLng
)
=>
void
;
onLatChange
:
(
value
:
string
)
=>
void
;
onLngChange
:
(
value
:
string
)
=>
void
;
onUpdateCoordinate
:
(
type
:
"lat"
|
"lng"
,
value
:
string
)
=>
void
;
onPlaceholderChange
:
(
value
:
string
)
=>
void
;
onCancel
:
()
=>
void
;
onConfirm
:
()
=>
void
;
...
...
@@ -28,8 +27,7 @@ export const LocationDialog = ({
state
,
locationInitialized
,
onPositionChange
,
onLatChange
,
onLngChange
,
onUpdateCoordinate
,
onPlaceholderChange
,
onCancel
,
onConfirm
,
...
...
@@ -67,7 +65,7 @@ export const LocationDialog = ({
min=
"-90"
max=
"90"
value=
{
latInput
}
onChange=
{
(
e
)
=>
on
LatChange
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
on
UpdateCoordinate
(
"lat"
,
e
.
target
.
value
)
}
className=
"h-9"
/>
</
div
>
...
...
@@ -83,7 +81,7 @@ export const LocationDialog = ({
min=
"-180"
max=
"180"
value=
{
lngInput
}
onChange=
{
(
e
)
=>
on
LngChange
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
on
UpdateCoordinate
(
"lng"
,
e
.
target
.
value
)
}
className=
"h-9"
/>
</
div
>
...
...
web/src/components/MemoEditor/components/index.ts
View file @
8a7c9767
// UI components for MemoEditor
export
{
default
as
ErrorBoundary
}
from
"./ErrorBoundary"
;
export
{
FocusModeExitButton
,
FocusModeOverlay
}
from
"./FocusModeOverlay"
;
export
{
LinkMemoDialog
}
from
"./LinkMemoDialog"
;
export
{
LocationDialog
}
from
"./LocationDialog"
;
web/src/components/MemoEditor/hooks/useLocation.ts
View file @
8a7c9767
...
...
@@ -23,25 +23,16 @@ export const useLocation = (initialLocation?: Location) => {
};
const
handlePositionChange
=
(
position
:
LatLng
)
=>
{
if
(
!
locationInitialized
)
{
setLocationInitialized
(
true
);
}
if
(
!
locationInitialized
)
setLocationInitialized
(
true
);
updatePosition
(
position
);
};
const
handleLatChange
=
(
value
:
string
)
=>
{
setState
((
prev
)
=>
({
...
prev
,
latInput
:
value
}));
const
lat
=
parseFloat
(
value
);
if
(
!
isNaN
(
lat
)
&&
lat
>=
-
90
&&
lat
<=
90
&&
state
.
position
)
{
updatePosition
(
new
LatLng
(
lat
,
state
.
position
.
lng
));
}
};
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
));
const
updateCoordinate
=
(
type
:
"lat"
|
"lng"
,
value
:
string
)
=>
{
setState
((
prev
)
=>
({
...
prev
,
[
type
===
"lat"
?
"latInput"
:
"lngInput"
]:
value
}));
const
num
=
parseFloat
(
value
);
const
isValid
=
type
===
"lat"
?
!
isNaN
(
num
)
&&
num
>=
-
90
&&
num
<=
90
:
!
isNaN
(
num
)
&&
num
>=
-
180
&&
num
<=
180
;
if
(
isValid
&&
state
.
position
)
{
updatePosition
(
type
===
"lat"
?
new
LatLng
(
num
,
state
.
position
.
lng
)
:
new
LatLng
(
state
.
position
.
lat
,
num
));
}
};
...
...
@@ -74,8 +65,7 @@ export const useLocation = (initialLocation?: Location) => {
state
,
locationInitialized
,
handlePositionChange
,
handleLatChange
,
handleLngChange
,
updateCoordinate
,
setPlaceholder
,
reset
,
getLocation
,
...
...
web/src/components/MemoEditor/index.tsx
View file @
8a7c9767
...
...
@@ -14,7 +14,7 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
DateTimeInput
from
"../DateTimeInput"
;
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
Editor
,
{
type
EditorRefActions
}
from
"./Editor"
;
import
{
...
...
@@ -224,113 +224,111 @@ const MemoEditor = observer((props: Props) => {
const
allowSave
=
(
hasContent
||
attachmentList
.
length
>
0
||
localFiles
.
length
>
0
)
&&
!
isUploadingAttachment
&&
!
isRequesting
;
return
(
<
ErrorBoundary
>
<
MemoEditorContext
.
Provider
value=
{
{
attachmentList
,
relationList
,
setAttachmentList
,
addLocalFiles
:
(
files
)
=>
addFiles
(
Array
.
from
(
files
.
map
((
f
)
=>
f
.
file
))),
removeLocalFile
:
removeFile
,
localFiles
,
setRelationList
,
memoName
,
}
}
>
{
/* Focus Mode Backdrop */
}
<
FocusModeOverlay
isActive=
{
isFocusMode
}
onToggle=
{
toggleFocusMode
}
/>
<
MemoEditorContext
.
Provider
value=
{
{
attachmentList
,
relationList
,
setAttachmentList
,
addLocalFiles
:
(
files
)
=>
addFiles
(
Array
.
from
(
files
.
map
((
f
)
=>
f
.
file
))),
removeLocalFile
:
removeFile
,
localFiles
,
setRelationList
,
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 Exit Button */
}
<
FocusModeExitButton
isActive=
{
isFocusMode
}
onToggle=
{
toggleFocusMode
}
title=
{
t
(
"editor.exit-focus-mode"
)
}
/>
<
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 Exit Button */
}
<
FocusModeExitButton
isActive=
{
isFocusMode
}
onToggle=
{
toggleFocusMode
}
title=
{
t
(
"editor.exit-focus-mode"
)
}
/>
<
Editor
ref=
{
editorRef
}
{
...
editorConfig
}
/>
<
LocationDisplay
mode=
"edit"
location=
{
location
}
onRemove=
{
()
=>
setLocation
(
undefined
)
}
/>
{
/* Show attachments and pending files together */
}
<
AttachmentList
mode=
"edit"
attachments=
{
attachmentList
}
onAttachmentsChange=
{
setAttachmentList
}
localFiles=
{
localFiles
}
onRemoveLocalFile=
{
removeFile
}
/>
<
RelationList
mode=
"edit"
relations=
{
referenceRelations
}
onRelationsChange=
{
setRelationList
}
/>
<
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"
>
<
InsertMenu
isUploading=
{
isUploadingAttachment
}
location=
{
location
}
onLocationChange=
{
setLocation
}
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"
)
}
<
Editor
ref=
{
editorRef
}
{
...
editorConfig
}
/>
<
LocationDisplay
mode=
"edit"
location=
{
location
}
onRemove=
{
()
=>
setLocation
(
undefined
)
}
/>
{
/* Show attachments and pending files together */
}
<
AttachmentList
mode=
"edit"
attachments=
{
attachmentList
}
onAttachmentsChange=
{
setAttachmentList
}
localFiles=
{
localFiles
}
onRemoveLocalFile=
{
removeFile
}
/>
<
RelationList
mode=
"edit"
relations=
{
referenceRelations
}
onRelationsChange=
{
setRelationList
}
/>
<
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"
>
<
InsertMenu
isUploading=
{
isUploadingAttachment
}
location=
{
location
}
onLocationChange=
{
setLocation
}
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
>
</
div
>
)
}
<
Button
disabled=
{
!
allowSave
||
isRequesting
}
onClick=
{
handleSaveBtnClick
}
>
{
isRequesting
?
<
LoaderIcon
className=
"w-4 h-4 animate-spin"
/>
:
t
(
"editor.save"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Show memo metadata if memoName is provided */
}
{
memoName
&&
(
<
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"
>
{
!
isEqual
(
createTime
,
updateTime
)
&&
updateTime
&&
(
<>
<
span
className=
"text-left"
>
Updated
</
span
>
<
DateTimeInput
value=
{
updateTime
}
onChange=
{
setUpdateTime
}
/>
</>
)
}
{
createTime
&&
(
<>
<
span
className=
"text-left"
>
Created
</
span
>
<
DateTimeInput
value=
{
createTime
}
onChange=
{
setCreateTime
}
/>
</>
)
}
<
span
className=
"text-left"
>
ID
</
span
>
<
button
type=
"button"
className=
"px-1 border border-transparent cursor-default text-left"
onClick=
{
()
=>
{
copy
(
extractMemoIdFromName
(
memoName
));
toast
.
success
(
t
(
"message.copied"
));
}
}
>
{
extractMemoIdFromName
(
memoName
)
}
</
button
>
</
div
>
{
/* Show memo metadata if memoName is provided */
}
{
memoName
&&
(
<
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"
>
{
!
isEqual
(
createTime
,
updateTime
)
&&
updateTime
&&
(
<>
<
span
className=
"text-left"
>
Updated
</
span
>
<
DateTimeInput
value=
{
updateTime
}
onChange=
{
setUpdateTime
}
/>
</>
)
}
{
createTime
&&
(
<>
<
span
className=
"text-left"
>
Created
</
span
>
<
DateTimeInput
value=
{
createTime
}
onChange=
{
setCreateTime
}
/>
</>
)
}
<
span
className=
"text-left"
>
ID
</
span
>
<
button
type=
"button"
className=
"px-1 border border-transparent cursor-default text-left"
onClick=
{
()
=>
{
copy
(
extractMemoIdFromName
(
memoName
));
toast
.
success
(
t
(
"message.copied"
));
}
}
>
{
extractMemoIdFromName
(
memoName
)
}
</
button
>
</
div
>
)
}
</
MemoEditorContext
.
Provider
>
</
ErrorBoundary
>
</
div
>
)
}
</
MemoEditorContext
.
Provider
>
);
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment