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
6402618c
Commit
6402618c
authored
Feb 11, 2026
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(web): replace EditableTimestamp with inline editor timestamp popover
parent
566fdcca
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
111 additions
and
130 deletions
+111
-130
EditableTimestamp.tsx
web/src/components/EditableTimestamp.tsx
+0
-104
MemoDetailSidebar.tsx
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
+3
-25
TimestampPopover.tsx
...src/components/MemoEditor/components/TimestampPopover.tsx
+89
-0
index.ts
web/src/components/MemoEditor/components/index.ts
+1
-0
index.tsx
web/src/components/MemoEditor/index.tsx
+3
-1
actions.ts
web/src/components/MemoEditor/state/actions.ts
+5
-0
reducer.ts
web/src/components/MemoEditor/state/reducer.ts
+9
-0
types.ts
web/src/components/MemoEditor/state/types.ts
+1
-0
No files found.
web/src/components/EditableTimestamp.tsx
deleted
100644 → 0
View file @
566fdcca
import
{
Timestamp
,
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
PencilIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
Props
{
timestamp
:
Timestamp
|
undefined
;
onChange
:
(
date
:
Date
)
=>
void
;
className
?:
string
;
}
const
EditableTimestamp
=
({
timestamp
,
onChange
,
className
}:
Props
)
=>
{
const
[
isEditing
,
setIsEditing
]
=
useState
(
false
);
const
[
inputValue
,
setInputValue
]
=
useState
(
""
);
const
inputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
date
=
timestamp
?
timestampDate
(
timestamp
)
:
new
Date
();
const
displayValue
=
date
.
toLocaleString
();
// Format date for datetime-local input (YYYY-MM-DDTHH:mm)
const
formatForInput
=
(
d
:
Date
):
string
=>
{
const
year
=
d
.
getFullYear
();
const
month
=
String
(
d
.
getMonth
()
+
1
).
padStart
(
2
,
"0"
);
const
day
=
String
(
d
.
getDate
()).
padStart
(
2
,
"0"
);
const
hours
=
String
(
d
.
getHours
()).
padStart
(
2
,
"0"
);
const
minutes
=
String
(
d
.
getMinutes
()).
padStart
(
2
,
"0"
);
return
`
${
year
}
-
${
month
}
-
${
day
}
T
${
hours
}
:
${
minutes
}
`
;
};
useEffect
(()
=>
{
if
(
isEditing
&&
inputRef
.
current
)
{
inputRef
.
current
.
focus
();
inputRef
.
current
.
showPicker
?.();
// Open datetime picker if available
}
},
[
isEditing
]);
const
handleEdit
=
()
=>
{
setInputValue
(
formatForInput
(
date
));
setIsEditing
(
true
);
};
const
handleSave
=
()
=>
{
if
(
!
inputValue
)
{
setIsEditing
(
false
);
return
;
}
const
newDate
=
new
Date
(
inputValue
);
if
(
isNaN
(
newDate
.
getTime
()))
{
toast
.
error
(
"Invalid date format"
);
return
;
}
onChange
(
newDate
);
setIsEditing
(
false
);
};
const
handleCancel
=
()
=>
{
setIsEditing
(
false
);
setInputValue
(
""
);
};
const
handleKeyDown
=
(
e
:
React
.
KeyboardEvent
<
HTMLInputElement
>
)
=>
{
if
(
e
.
key
===
"Enter"
)
{
handleSave
();
}
else
if
(
e
.
key
===
"Escape"
)
{
handleCancel
();
}
};
if
(
isEditing
)
{
return
(
<
input
ref=
{
inputRef
}
type=
"datetime-local"
value=
{
inputValue
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
onBlur=
{
handleSave
}
onKeyDown=
{
handleKeyDown
}
className=
{
cn
(
"w-full px-2 py-1.5 text-sm text-foreground bg-background rounded-md border border-border outline-none transition-all focus:border-ring focus:ring-1 focus:ring-ring/20"
,
className
,
)
}
/>
);
}
return
(
<
button
type=
"button"
onClick=
{
handleEdit
}
className=
{
cn
(
"group w-full text-left px-2 py-1.5 text-sm text-foreground/80 rounded-md transition-all flex items-center justify-between hover:bg-accent/50 hover:text-foreground"
,
className
,
)
}
>
<
span
className=
"font-normal"
>
{
displayValue
}
</
span
>
<
PencilIcon
className=
"w-3.5 h-3.5 opacity-0 group-hover:opacity-40 transition-opacity shrink-0 text-muted-foreground"
/>
</
button
>
);
};
export
default
EditableTimestamp
;
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
View file @
6402618c
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
timestamp
From
Date
}
from
"@bufbuild/protobuf/wkt"
;
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
CheckCircleIcon
,
Code2Icon
,
HashIcon
,
LinkIcon
}
from
"lucide-react"
;
import
{
CheckCircleIcon
,
Code2Icon
,
HashIcon
,
LinkIcon
}
from
"lucide-react"
;
import
toast
from
"react-hot-toast"
;
import
EditableTimestamp
from
"@/components/EditableTimestamp"
;
import
{
useUpdateMemo
}
from
"@/hooks/useMemoQueries"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Memo
,
Memo_PropertySchema
,
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
Memo
,
Memo_PropertySchema
,
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
@@ -18,29 +15,10 @@ interface Props {
...
@@ -18,29 +15,10 @@ interface Props {
const
MemoDetailSidebar
=
({
memo
,
className
,
parentPage
}:
Props
)
=>
{
const
MemoDetailSidebar
=
({
memo
,
className
,
parentPage
}:
Props
)
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
const
{
mutate
:
updateMemo
}
=
useUpdateMemo
();
const
property
=
create
(
Memo_PropertySchema
,
memo
.
property
||
{});
const
property
=
create
(
Memo_PropertySchema
,
memo
.
property
||
{});
const
hasSpecialProperty
=
property
.
hasLink
||
property
.
hasTaskList
||
property
.
hasCode
;
const
hasSpecialProperty
=
property
.
hasLink
||
property
.
hasTaskList
||
property
.
hasCode
;
const
hasReferenceRelations
=
memo
.
relations
.
some
((
r
)
=>
r
.
type
===
MemoRelation_Type
.
REFERENCE
);
const
hasReferenceRelations
=
memo
.
relations
.
some
((
r
)
=>
r
.
type
===
MemoRelation_Type
.
REFERENCE
);
const
handleUpdateTimestamp
=
(
field
:
"createTime"
|
"updateTime"
,
date
:
Date
)
=>
{
const
currentTimestamp
=
memo
[
field
];
const
newTimestamp
=
timestampFromDate
(
date
);
if
(
isEqual
(
currentTimestamp
,
newTimestamp
))
{
return
;
}
updateMemo
(
{
update
:
{
name
:
memo
.
name
,
[
field
]:
newTimestamp
},
updateMask
:
[
field
===
"createTime"
?
"create_time"
:
"update_time"
],
},
{
onSuccess
:
()
=>
toast
.
success
(
"Updated successfully"
),
onError
:
(
error
)
=>
toast
.
error
(
error
.
message
),
},
);
};
return
(
return
(
<
aside
className=
{
cn
(
"relative w-full h-auto max-h-screen overflow-auto flex flex-col justify-start items-start"
,
className
)
}
>
<
aside
className=
{
cn
(
"relative w-full h-auto max-h-screen overflow-auto flex flex-col justify-start items-start"
,
className
)
}
>
<
div
className=
"flex flex-col justify-start items-start w-full gap-4 h-auto shrink-0 flex-nowrap"
>
<
div
className=
"flex flex-col justify-start items-start w-full gap-4 h-auto shrink-0 flex-nowrap"
>
...
@@ -56,13 +34,13 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
...
@@ -56,13 +34,13 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
<
div
className=
"w-full space-y-1"
>
<
div
className=
"w-full space-y-1"
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1"
>
{
t
(
"common.created-at"
)
}
</
p
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1"
>
{
t
(
"common.created-at"
)
}
</
p
>
<
EditableTimestamp
timestamp=
{
memo
.
createTime
}
onChange=
{
(
date
)
=>
handleUpdateTimestamp
(
"createTime"
,
date
)
}
/
>
<
p
className=
"text-sm text-muted-foreground px-1"
>
{
memo
.
createTime
?
timestampDate
(
memo
.
createTime
).
toLocaleString
()
:
"-"
}
</
p
>
</
div
>
</
div
>
{
!
isEqual
(
memo
.
createTime
,
memo
.
updateTime
)
&&
(
{
!
isEqual
(
memo
.
createTime
,
memo
.
updateTime
)
&&
(
<
div
className=
"w-full space-y-1"
>
<
div
className=
"w-full space-y-1"
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1"
>
{
t
(
"common.last-updated-at"
)
}
</
p
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1"
>
{
t
(
"common.last-updated-at"
)
}
</
p
>
<
EditableTimestamp
timestamp=
{
memo
.
updateTime
}
onChange=
{
(
date
)
=>
handleUpdateTimestamp
(
"updateTime"
,
date
)
}
/
>
<
p
className=
"text-sm text-muted-foreground px-1"
>
{
memo
.
updateTime
?
timestampDate
(
memo
.
updateTime
).
toLocaleString
()
:
"-"
}
</
p
>
</
div
>
</
div
>
)
}
)
}
...
...
web/src/components/MemoEditor/components/TimestampPopover.tsx
0 → 100644
View file @
6402618c
import
{
type
FC
,
useRef
,
useState
}
from
"react"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useEditorContext
}
from
"../state"
;
const
DATETIME_FORMAT
=
"YYYY-MM-DD HH:mm:ss"
;
function
formatDate
(
date
:
Date
):
string
{
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
"0"
);
return
`
${
date
.
getFullYear
()}
-
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
${
pad
(
date
.
getHours
())}
:
${
pad
(
date
.
getMinutes
())}
:
${
pad
(
date
.
getSeconds
())}
`
;
}
function
parseDate
(
value
:
string
):
Date
|
undefined
{
const
match
=
value
.
match
(
/^
(\d{4})
-
(\d{2})
-
(\d{2})
(\d{2})
:
(\d{2})
:
(\d{2})
$/
);
if
(
!
match
)
return
undefined
;
const
date
=
new
Date
(
Number
(
match
[
1
]),
Number
(
match
[
2
])
-
1
,
Number
(
match
[
3
]),
Number
(
match
[
4
]),
Number
(
match
[
5
]),
Number
(
match
[
6
]));
return
Number
.
isNaN
(
date
.
getTime
())
?
undefined
:
date
;
}
const
TimestampInput
:
FC
<
{
label
:
string
;
date
:
Date
|
undefined
;
onChange
:
(
date
:
Date
)
=>
void
;
}
>
=
({
label
,
date
,
onChange
})
=>
{
const
initialValue
=
useRef
(
date
?
formatDate
(
date
)
:
""
);
const
[
value
,
setValue
]
=
useState
(
initialValue
.
current
);
const
[
invalid
,
setInvalid
]
=
useState
(
false
);
const
handleBlur
=
()
=>
{
const
parsed
=
parseDate
(
value
);
if
(
parsed
)
{
setInvalid
(
false
);
onChange
(
parsed
);
}
else
{
setInvalid
(
true
);
}
};
return
(
<
div
className=
"space-y-1"
>
<
label
className=
"text-xs font-medium text-muted-foreground"
>
{
label
}
{
value
!==
initialValue
.
current
&&
<
span
className=
"text-primary ml-0.5"
>
*
</
span
>
}
</
label
>
<
input
type=
"text"
className=
"block w-full rounded-md border border-border bg-background px-2 py-1 text-sm font-mono data-[invalid=true]:border-destructive"
data
-
invalid=
{
invalid
}
placeholder=
{
DATETIME_FORMAT
}
value=
{
value
}
onChange=
{
(
e
)
=>
setValue
(
e
.
target
.
value
)
}
onBlur=
{
handleBlur
}
/>
</
div
>
);
};
export
const
TimestampPopover
:
FC
=
()
=>
{
const
t
=
useTranslate
();
const
{
state
,
actions
,
dispatch
}
=
useEditorContext
();
const
{
createTime
,
updateTime
}
=
state
.
timestamps
;
if
(
!
createTime
)
return
null
;
return
(
<
Popover
>
<
PopoverTrigger
asChild
>
<
button
type=
"button"
className=
"w-full text-sm text-muted-foreground -mb-1 text-left hover:text-foreground transition-colors cursor-pointer"
>
{
formatDate
(
createTime
)
}
</
button
>
</
PopoverTrigger
>
<
PopoverContent
align=
"start"
className=
"w-auto p-2 pt-1 space-y-1"
>
<
TimestampInput
label=
{
t
(
"common.created-at"
)
}
date=
{
createTime
}
onChange=
{
(
d
)
=>
dispatch
(
actions
.
setTimestamps
({
createTime
:
d
}))
}
/>
<
TimestampInput
label=
{
t
(
"common.last-updated-at"
)
}
date=
{
updateTime
}
onChange=
{
(
d
)
=>
dispatch
(
actions
.
setTimestamps
({
updateTime
:
d
}))
}
/>
</
PopoverContent
>
</
Popover
>
);
};
web/src/components/MemoEditor/components/index.ts
View file @
6402618c
...
@@ -9,3 +9,4 @@ export { LinkMemoDialog } from "./LinkMemoDialog";
...
@@ -9,3 +9,4 @@ export { LinkMemoDialog } from "./LinkMemoDialog";
export
{
LocationDialog
}
from
"./LocationDialog"
;
export
{
LocationDialog
}
from
"./LocationDialog"
;
export
{
default
as
LocationDisplay
}
from
"./LocationDisplay"
;
export
{
default
as
LocationDisplay
}
from
"./LocationDisplay"
;
export
{
default
as
RelationList
}
from
"./RelationList"
;
export
{
default
as
RelationList
}
from
"./RelationList"
;
export
{
TimestampPopover
}
from
"./TimestampPopover"
;
web/src/components/MemoEditor/index.tsx
View file @
6402618c
...
@@ -9,7 +9,7 @@ import { handleError } from "@/lib/error";
...
@@ -9,7 +9,7 @@ import { handleError } from "@/lib/error";
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityFromString
}
from
"@/utils/memo"
;
import
{
convertVisibilityFromString
}
from
"@/utils/memo"
;
import
{
EditorContent
,
EditorMetadata
,
EditorToolbar
,
FocusModeExitButton
,
FocusModeOverlay
}
from
"./components"
;
import
{
EditorContent
,
EditorMetadata
,
EditorToolbar
,
FocusModeExitButton
,
FocusModeOverlay
,
TimestampPopover
}
from
"./components"
;
import
{
FOCUS_MODE_STYLES
}
from
"./constants"
;
import
{
FOCUS_MODE_STYLES
}
from
"./constants"
;
import
type
{
EditorRefActions
}
from
"./Editor"
;
import
type
{
EditorRefActions
}
from
"./Editor"
;
import
{
useAutoSave
,
useFocusMode
,
useKeyboard
,
useMemoInit
}
from
"./hooks"
;
import
{
useAutoSave
,
useFocusMode
,
useKeyboard
,
useMemoInit
}
from
"./hooks"
;
...
@@ -141,6 +141,8 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
...
@@ -141,6 +141,8 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
{
/* Exit button is absolutely positioned in top-right corner when active */
}
{
/* Exit button is absolutely positioned in top-right corner when active */
}
<
FocusModeExitButton
isActive=
{
state
.
ui
.
isFocusMode
}
onToggle=
{
handleToggleFocusMode
}
title=
{
t
(
"editor.exit-focus-mode"
)
}
/>
<
FocusModeExitButton
isActive=
{
state
.
ui
.
isFocusMode
}
onToggle=
{
handleToggleFocusMode
}
title=
{
t
(
"editor.exit-focus-mode"
)
}
/>
{
memoName
&&
<
TimestampPopover
/>
}
{
/* Editor content grows to fill available space in focus mode */
}
{
/* Editor content grows to fill available space in focus mode */
}
<
EditorContent
ref=
{
editorRef
}
placeholder=
{
placeholder
}
autoFocus=
{
autoFocus
}
/>
<
EditorContent
ref=
{
editorRef
}
placeholder=
{
placeholder
}
autoFocus=
{
autoFocus
}
/>
...
...
web/src/components/MemoEditor/state/actions.ts
View file @
6402618c
...
@@ -72,6 +72,11 @@ export const editorActions = {
...
@@ -72,6 +72,11 @@ export const editorActions = {
payload
:
value
,
payload
:
value
,
}),
}),
setTimestamps
:
(
timestamps
:
Partial
<
EditorState
[
"timestamps"
]
>
):
EditorAction
=>
({
type
:
"SET_TIMESTAMPS"
,
payload
:
timestamps
,
}),
reset
:
():
EditorAction
=>
({
reset
:
():
EditorAction
=>
({
type
:
"RESET"
,
type
:
"RESET"
,
}),
}),
...
...
web/src/components/MemoEditor/state/reducer.ts
View file @
6402618c
...
@@ -119,6 +119,15 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
...
@@ -119,6 +119,15 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
},
},
};
};
case
"SET_TIMESTAMPS"
:
return
{
...
state
,
timestamps
:
{
...
state
.
timestamps
,
...
action
.
payload
,
},
};
case
"RESET"
:
case
"RESET"
:
return
{
return
{
...
initialState
,
...
initialState
,
...
...
web/src/components/MemoEditor/state/types.ts
View file @
6402618c
...
@@ -45,6 +45,7 @@ export type EditorAction =
...
@@ -45,6 +45,7 @@ export type EditorAction =
|
{
type
:
"SET_LOADING"
;
payload
:
{
key
:
LoadingKey
;
value
:
boolean
}
}
|
{
type
:
"SET_LOADING"
;
payload
:
{
key
:
LoadingKey
;
value
:
boolean
}
}
|
{
type
:
"SET_DRAGGING"
;
payload
:
boolean
}
|
{
type
:
"SET_DRAGGING"
;
payload
:
boolean
}
|
{
type
:
"SET_COMPOSING"
;
payload
:
boolean
}
|
{
type
:
"SET_COMPOSING"
;
payload
:
boolean
}
|
{
type
:
"SET_TIMESTAMPS"
;
payload
:
Partial
<
EditorState
[
"timestamps"
]
>
}
|
{
type
:
"RESET"
};
|
{
type
:
"RESET"
};
export
const
initialState
:
EditorState
=
{
export
const
initialState
:
EditorState
=
{
...
...
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