diff --git a/web/src/components/EditableTimestamp.tsx b/web/src/components/EditableTimestamp.tsx new file mode 100644 index 000000000..73b840247 --- /dev/null +++ b/web/src/components/EditableTimestamp.tsx @@ -0,0 +1,104 @@ +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(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) => { + if (e.key === "Enter") { + handleSave(); + } else if (e.key === "Escape") { + handleCancel(); + } + }; + + if (isEditing) { + return ( + 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 ( + + ); +}; + +export default EditableTimestamp; diff --git a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx index f236d0e84..00e7e3f84 100644 --- a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx +++ b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx @@ -1,7 +1,10 @@ import { create } from "@bufbuild/protobuf"; -import { timestampDate } from "@bufbuild/protobuf/wkt"; +import { timestampFromDate } 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 (