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
6731eccd
Commit
6731eccd
authored
Jan 26, 2026
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add EditableTimestamp component for inline date editing in MemoDetailSidebar
parent
a7b0d71f
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
159 additions
and
44 deletions
+159
-44
EditableTimestamp.tsx
web/src/components/EditableTimestamp.tsx
+104
-0
MemoDetailSidebar.tsx
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
+55
-44
No files found.
web/src/components/EditableTimestamp.tsx
0 → 100644
View file @
6731eccd
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 @
6731eccd
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
timestamp
From
Date
}
from
"@bufbuild/protobuf/wkt"
;
import
{
isEqual
}
from
"lodash-es"
;
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
{
Memo
,
Memo_PropertySchema
,
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -18,84 +21,92 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
const
property
=
create
(
Memo_PropertySchema
,
memo
.
property
||
{});
const
hasSpecialProperty
=
property
.
hasLink
||
property
.
hasTaskList
||
property
.
hasCode
||
property
.
hasIncompleteTasks
;
const
shouldShowRelationGraph
=
memo
.
relations
.
filter
((
r
)
=>
r
.
type
===
MemoRelation_Type
.
REFERENCE
).
length
>
0
;
const
{
mutate
:
updateMemo
}
=
useUpdateMemo
();
const
handleUpdateTimestamp
=
(
field
:
"createTime"
|
"updateTime"
,
date
:
Date
)
=>
{
const
timestamp
=
timestampFromDate
(
date
);
updateMemo
(
{
update
:
{
name
:
memo
.
name
,
[
field
]:
timestamp
,
},
updateMask
:
[
field
===
"createTime"
?
"create_time"
:
"update_time"
],
},
{
onSuccess
:
()
=>
{
toast
.
success
(
"Updated successfully"
);
},
onError
:
(
error
)
=>
{
toast
.
error
(
error
.
message
);
},
},
);
};
return
(
<
aside
className=
{
cn
(
"relative w-full h-auto max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start"
,
className
)
}
>
<
div
className=
"flex flex-col justify-start items-start w-full
px-1 gap-2
h-auto shrink-0 flex-nowrap hide-scrollbar"
>
<
div
className=
"flex flex-col justify-start items-start w-full
gap-4
h-auto shrink-0 flex-nowrap hide-scrollbar"
>
{
shouldShowRelationGraph
&&
(
<
div
className=
"relative w-full h-36 border border-border rounded-lg bg-muted"
>
<
div
className=
"relative w-full h-36 border border-border rounded-lg bg-muted
overflow-hidden
"
>
<
MemoRelationForceGraph
className=
"w-full h-full"
memo=
{
memo
}
parentPage=
{
parentPage
}
/>
<
div
className=
"absolute top-
1 left-2 text-xs opacity-60 font-mono
gap-1 flex flex-row items-center"
>
<
div
className=
"absolute top-
2 left-2 text-xs text-muted-foreground/60 font-medium
gap-1 flex flex-row items-center"
>
<
span
>
{
t
(
"common.relations"
)
}
</
span
>
<
span
className=
"text-xs opacity-60"
>
(Beta)
</
span
>
</
div
>
</
div
>
)
}
<
div
className=
"w-full flex flex-col"
>
<
p
className=
"flex flex-row justify-start items-center w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none"
>
<
span
>
{
t
(
"common.created-at"
)
}
</
span
>
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
memo
.
createTime
&&
timestampDate
(
memo
.
createTime
).
toLocaleString
()
}
</
p
>
<
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
>
<
EditableTimestamp
timestamp=
{
memo
.
createTime
}
onChange=
{
(
date
)
=>
handleUpdateTimestamp
(
"createTime"
,
date
)
}
/>
</
div
>
{
!
isEqual
(
memo
.
createTime
,
memo
.
updateTime
)
&&
(
<
div
className=
"w-full flex flex-col"
>
<
p
className=
"flex flex-row justify-start items-center w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none"
>
<
span
>
{
t
(
"common.last-updated-at"
)
}
</
span
>
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
memo
.
updateTime
&&
timestampDate
(
memo
.
updateTime
).
toLocaleString
()
}
</
p
>
<
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
>
<
EditableTimestamp
timestamp=
{
memo
.
updateTime
}
onChange=
{
(
date
)
=>
handleUpdateTimestamp
(
"updateTime"
,
date
)
}
/>
</
div
>
)
}
{
hasSpecialProperty
&&
(
<
div
className=
"w-full flex flex-col"
>
<
p
className=
"flex flex-row justify-start items-center w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none"
>
<
span
>
{
t
(
"common.properties"
)
}
</
span
>
</
p
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap text-muted-foreground"
>
<
div
className=
"w-full space-y-2"
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1"
>
{
t
(
"common.properties"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-2 flex-wrap px-1"
>
{
property
.
hasLink
&&
(
<
div
className=
"w-auto border border-border pl-1 pr-1.5 rounded-md flex justify-between items-center"
>
<
div
className=
"w-auto flex justify-start items-center mr-1"
>
<
LinkIcon
className=
"w-4 h-auto mr-1"
/>
<
span
className=
"block text-sm"
>
{
t
(
"memo.links"
)
}
</
span
>
</
div
>
<
div
className=
"inline-flex items-center gap-1.5 px-2 py-1 bg-muted/50 border border-border/50 rounded-md text-xs text-muted-foreground"
>
<
LinkIcon
className=
"w-3.5 h-3.5"
/>
<
span
>
{
t
(
"memo.links"
)
}
</
span
>
</
div
>
)
}
{
property
.
hasTaskList
&&
(
<
div
className=
"w-auto border border-border pl-1 pr-1.5 rounded-md flex justify-between items-center"
>
<
div
className=
"w-auto flex justify-start items-center mr-1"
>
<
CheckCircleIcon
className=
"w-4 h-auto mr-1"
/>
<
span
className=
"block text-sm"
>
{
t
(
"memo.to-do"
)
}
</
span
>
</
div
>
<
div
className=
"inline-flex items-center gap-1.5 px-2 py-1 bg-muted/50 border border-border/50 rounded-md text-xs text-muted-foreground"
>
<
CheckCircleIcon
className=
"w-3.5 h-3.5"
/>
<
span
>
{
t
(
"memo.to-do"
)
}
</
span
>
</
div
>
)
}
{
property
.
hasCode
&&
(
<
div
className=
"w-auto border border-border pl-1 pr-1.5 rounded-md flex justify-between items-center"
>
<
div
className=
"w-auto flex justify-start items-center mr-1"
>
<
Code2Icon
className=
"w-4 h-auto mr-1"
/>
<
span
className=
"block text-sm"
>
{
t
(
"memo.code"
)
}
</
span
>
</
div
>
<
div
className=
"inline-flex items-center gap-1.5 px-2 py-1 bg-muted/50 border border-border/50 rounded-md text-xs text-muted-foreground"
>
<
Code2Icon
className=
"w-3.5 h-3.5"
/>
<
span
>
{
t
(
"memo.code"
)
}
</
span
>
</
div
>
)
}
</
div
>
</
div
>
)
}
{
memo
.
tags
.
length
>
0
&&
(
<
div
className=
"w-full"
>
<
div
className=
"flex flex-row justify-start items-center
w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none
"
>
<
span
>
{
t
(
"common.tags"
)
}
</
span
>
<
span
className=
"
shrink-
0"
>
(
{
memo
.
tags
.
length
}
)
</
span
>
<
div
className=
"w-full
space-y-2
"
>
<
div
className=
"flex flex-row justify-start items-center
gap-1.5 px-1
"
>
<
p
className=
"text-xs font-medium text-muted-foreground/60 uppercase tracking-wide"
>
{
t
(
"common.tags"
)
}
</
p
>
<
span
className=
"
text-xs text-muted-foreground/4
0"
>
(
{
memo
.
tags
.
length
}
)
</
span
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center
relative flex-wrap gap-x-2 gap-y
-1"
>
<
div
className=
"w-full flex flex-row justify-start items-center
flex-wrap gap-1.5 px
-1"
>
{
memo
.
tags
.
map
((
tag
)
=>
(
<
div
key=
{
tag
}
className=
"
shrink-0 w-auto max-w-full text-sm rounded-md leading-6 flex flex-row justify-start items-center select-none hover:opacity-80 text-muted-foreground
"
className=
"
inline-flex items-center gap-1 px-2 py-0.5 bg-muted/50 border border-border/50 rounded-md text-xs text-muted-foreground hover:bg-muted transition-colors cursor-pointer group
"
>
<
HashIcon
className=
"group-hover:hidden w-4 h-auto shrink-0 opacity-40"
/>
<
div
className=
{
cn
(
"inline-flex flex-nowrap ml-0.5 gap-0.5 cursor-pointer max-w-[calc(100%-16px)]"
)
}
>
<
span
className=
"truncate opacity-80"
>
{
tag
}
</
span
>
</
div
>
<
HashIcon
className=
"w-3 h-3 opacity-40 group-hover:opacity-60 transition-opacity"
/>
<
span
className=
"opacity-80 group-hover:opacity-100 transition-opacity"
>
{
tag
}
</
span
>
</
div
>
))
}
</
div
>
...
...
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