From 8eb5ba9f9b82739fdcce6b396d38bc88c350f35e Mon Sep 17 00:00:00 2001 From: Giulio <3272563+giuliohome@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:13:44 +0100 Subject: [PATCH 1/5] Fix: Calendar date picker not updating Addresses bug #5415 where the calendar date picker was not updating correctly when a new date was selected. This fix involves: - Updating useDateFilterNavigation to correctly handle displayTime filters, ensuring existing filters are replaced with new selections. - Modifying CalendarCell to make all days interactive, allowing users to select any date in the current month, regardless of whether it has memos. - Passing the selected date to useCalendarMatrix in MonthCalendar to ensure the chosen date is visually highlighted. --- .../ActivityCalendar/CalendarCell.tsx | 4 ++-- .../ActivityCalendar/MonthCalendar.tsx | 7 +++++-- web/src/hooks/useDateFilterNavigation.ts | 20 +++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/web/src/components/ActivityCalendar/CalendarCell.tsx b/web/src/components/ActivityCalendar/CalendarCell.tsx index 48026e46013e2..f237eb89a2d23 100644 --- a/web/src/components/ActivityCalendar/CalendarCell.tsx +++ b/web/src/components/ActivityCalendar/CalendarCell.tsx @@ -17,7 +17,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => { const { day, maxCount, tooltipText, onClick, size = "default" } = props; const handleClick = () => { - if (day.count > 0 && onClick) { + if (onClick) { onClick(day.date); } }; @@ -31,7 +31,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => { sizeConfig.borderRadius, smallExtraClasses, ); - const isInteractive = Boolean(onClick && day.count > 0); + const isInteractive = Boolean(onClick); const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText; if (!day.isCurrentMonth) { diff --git a/web/src/components/ActivityCalendar/MonthCalendar.tsx b/web/src/components/ActivityCalendar/MonthCalendar.tsx index b1a9a973d597e..7eb041f0d73a7 100644 --- a/web/src/components/ActivityCalendar/MonthCalendar.tsx +++ b/web/src/components/ActivityCalendar/MonthCalendar.tsx @@ -1,5 +1,6 @@ -import { memo } from "react"; +import { memo, useMemo } from "react"; import { useInstance } from "@/contexts/InstanceContext"; +import { useMemoFilterContext } from "@/contexts/MemoFilterContext"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import { CalendarCell } from "./CalendarCell"; @@ -13,11 +14,13 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => { const { month, data, maxCount, size = "default", onClick, className } = props; const t = useTranslate(); const { generalSetting } = useInstance(); + const { getFiltersByFactor } = useMemoFilterContext(); const weekStartDayOffset = generalSetting.weekStartDayOffset; const today = useTodayDate(); const weekDays = useWeekdayLabels(); + const selectedDate = useMemo(() => getFiltersByFactor("displayTime")?.[0]?.value || "", [getFiltersByFactor]); const { weeks, weekDays: rotatedWeekDays } = useCalendarMatrix({ month, @@ -25,7 +28,7 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => { weekDays, weekStartDayOffset, today, - selectedDate: "", + selectedDate: selectedDate, }); const sizeConfig = size === "small" ? SMALL_CELL_SIZE : DEFAULT_CELL_SIZE; diff --git a/web/src/hooks/useDateFilterNavigation.ts b/web/src/hooks/useDateFilterNavigation.ts index ce0a167b68986..e52681164a6a3 100644 --- a/web/src/hooks/useDateFilterNavigation.ts +++ b/web/src/hooks/useDateFilterNavigation.ts @@ -1,16 +1,28 @@ import { useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { stringifyFilters } from "@/contexts/MemoFilterContext"; +import { MemoFilter, stringifyFilters, useMemoFilterContext } from "@/contexts/MemoFilterContext"; export const useDateFilterNavigation = () => { const navigate = useNavigate(); + const { filters } = useMemoFilterContext(); const navigateToDateFilter = useCallback( (date: string) => { - const filterQuery = stringifyFilters([{ factor: "displayTime", value: date }]); - navigate(`/?filter=${filterQuery}`); + const otherFilters = filters.filter((f) => f.factor !== "displayTime"); + const newFilters: MemoFilter[] = [...otherFilters]; + const existingDateFilter = filters.find((f) => f.factor === "displayTime"); + + // If the selected date is different from the current filter, add the new filter. + // If the selected date is the same, the filter is effectively removed. + if (existingDateFilter?.value !== date) { + newFilters.push({ factor: "displayTime", value: date }); + } + + const filterQuery = stringifyFilters(newFilters); + const targetUrl = filterQuery ? `/?filter=${filterQuery}` : "/"; + navigate(targetUrl); }, - [navigate], + [filters, navigate], ); return navigateToDateFilter; From e6ca236028ce30c2bbc6a65f5e7f4d67f48d2cb3 Mon Sep 17 00:00:00 2001 From: Giulio <3272563+giuliohome@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:18:44 +0100 Subject: [PATCH 2/5] fix #5415: Honor frontend-provided memo creation date --- server/router/api/v1/memo_service.go | 9 +++++++++ store/db/postgres/memo.go | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index f407b009e5fbb..88ccc971b74d7 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -45,6 +45,15 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR Content: request.Memo.Content, Visibility: convertVisibilityToStore(request.Memo.Visibility), } + if request.Memo.CreateTime != nil { + create.CreatedTs = request.Memo.CreateTime.AsTime().Unix() + } + // If UpdateTime is provided, use it. Otherwise, if CreateTime is provided, use it for UpdatedTs as well. + if request.Memo.UpdateTime != nil { + create.UpdatedTs = request.Memo.UpdateTime.AsTime().Unix() + } else if request.Memo.CreateTime != nil { + create.UpdatedTs = request.Memo.CreateTime.AsTime().Unix() + } instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting") diff --git a/store/db/postgres/memo.go b/store/db/postgres/memo.go index 3fa3abd4b10f0..ff416eac74dfe 100644 --- a/store/db/postgres/memo.go +++ b/store/db/postgres/memo.go @@ -14,7 +14,17 @@ import ( ) func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { - fields := []string{"uid", "creator_id", "content", "visibility", "payload"} + fields := []string{"uid", "creator_id", "content", "visibility"} + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility} + if create.CreatedTs != 0 { + fields = append(fields, "created_ts") + args = append(args, create.CreatedTs) + } + if create.UpdatedTs != 0 { + fields = append(fields, "updated_ts") + args = append(args, create.UpdatedTs) + } + payload := "{}" if create.Payload != nil { payloadBytes, err := protojson.Marshal(create.Payload) @@ -23,7 +33,8 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e } payload = string(payloadBytes) } - args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + fields = append(fields, "payload") + args = append(args, payload) stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts, updated_ts, row_status" if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( From 0b8752fb2b20f090d565d2526d9a08761fc135a6 Mon Sep 17 00:00:00 2001 From: Giulio <3272563+giuliohome@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:39:47 +0100 Subject: [PATCH 3/5] feat: Add "Created at" field to memo editor and sync with calendar filter This commit re-introduces the "Created at" date picker field in the memo editor. This field allows users to explicitly set the creation date of a new memo. Key changes include: - Reinstating the DateTimeInput component in bound to . - Ensuring localization key is present in to support the UI label. - Correctly initializing from the filter in when a new memo is created, if the field is not already set. - Re-adding the action and its corresponding reducer logic to manage updates to . - Removing the unused import from to fix linting errors. These changes ensure the frontend provides and manages the for memos, which will then be used by the backend's (forthcoming) fix to persist this value on initial memo creation. --- .../MemoEditor/components/EditorMetadata.tsx | 12 ++++++++++++ .../components/MemoEditor/hooks/useMemoInit.ts | 16 +++++++++++++--- web/src/components/MemoEditor/state/actions.ts | 5 +++++ web/src/components/MemoEditor/state/reducer.ts | 9 +++++++++ web/src/components/MemoEditor/state/types.ts | 1 + web/src/locales/en.json | 3 ++- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/web/src/components/MemoEditor/components/EditorMetadata.tsx b/web/src/components/MemoEditor/components/EditorMetadata.tsx index dc9ba17bd865e..bd2e0dcecf273 100644 --- a/web/src/components/MemoEditor/components/EditorMetadata.tsx +++ b/web/src/components/MemoEditor/components/EditorMetadata.tsx @@ -1,4 +1,6 @@ import type { FC } from "react"; +import DateTimeInput from "@/components/DateTimeInput"; +import { useTranslate } from "@/utils/i18n"; import { useEditorContext } from "../state"; import type { EditorMetadataProps } from "../types"; import AttachmentList from "./AttachmentList"; @@ -6,6 +8,7 @@ import LocationDisplay from "./LocationDisplay"; import RelationList from "./RelationList"; export const EditorMetadata: FC = () => { + const t = useTranslate(); const { state, actions, dispatch } = useEditorContext(); return ( @@ -22,6 +25,15 @@ export const EditorMetadata: FC = () => { {state.metadata.location && ( dispatch(actions.setMetadata({ location: undefined }))} /> )} +
+
+ {t("editor.created-at")}: + dispatch(actions.setTimestamps({ createTime: date }))} + /> +
+
); }; diff --git a/web/src/components/MemoEditor/hooks/useMemoInit.ts b/web/src/components/MemoEditor/hooks/useMemoInit.ts index d49a487291ebf..bac204f8ae8d1 100644 --- a/web/src/components/MemoEditor/hooks/useMemoInit.ts +++ b/web/src/components/MemoEditor/hooks/useMemoInit.ts @@ -1,4 +1,6 @@ +import dayjs from "dayjs"; import { useEffect, useRef } from "react"; +import { useMemoFilterContext } from "@/contexts/MemoFilterContext"; import type { EditorRefActions } from "../Editor"; import { cacheService, memoService } from "../services"; import { useEditorContext } from "../state"; @@ -10,7 +12,8 @@ export const useMemoInit = ( username: string, autoFocus?: boolean, ) => { - const { actions, dispatch } = useEditorContext(); + const { state, actions, dispatch } = useEditorContext(); + const { getFiltersByFactor } = useMemoFilterContext(); const initializedRef = useRef(false); useEffect(() => { @@ -32,7 +35,14 @@ export const useMemoInit = ( }), ); } else { - // Load from cache for new memo + // New memo: first apply date filter if not already set, then load from cache + if (!state.timestamps.createTime) { + const displayTimeFilter = getFiltersByFactor("displayTime")?.[0]?.value; + if (displayTimeFilter) { + dispatch(actions.setTimestamps({ createTime: dayjs(displayTimeFilter).toDate() })); + } + } + const cachedContent = cacheService.load(cacheService.key(username, cacheKey)); if (cachedContent) { dispatch(actions.updateContent(cachedContent)); @@ -52,5 +62,5 @@ export const useMemoInit = ( }; init(); - }, [memoName, cacheKey, username, autoFocus, actions, dispatch, editorRef]); + }, [memoName, cacheKey, username, autoFocus, actions, dispatch, editorRef, getFiltersByFactor, state.timestamps.createTime]); }; diff --git a/web/src/components/MemoEditor/state/actions.ts b/web/src/components/MemoEditor/state/actions.ts index ec46bd05a62fa..67700b5f986a3 100644 --- a/web/src/components/MemoEditor/state/actions.ts +++ b/web/src/components/MemoEditor/state/actions.ts @@ -19,6 +19,11 @@ export const editorActions = { payload: metadata, }), + setTimestamps: (timestamps: Partial): EditorAction => ({ + type: "SET_TIMESTAMPS", + payload: timestamps, + }), + addAttachment: (attachment: Attachment): EditorAction => ({ type: "ADD_ATTACHMENT", payload: attachment, diff --git a/web/src/components/MemoEditor/state/reducer.ts b/web/src/components/MemoEditor/state/reducer.ts index cc935f2bf4979..781056b2ffc4a 100644 --- a/web/src/components/MemoEditor/state/reducer.ts +++ b/web/src/components/MemoEditor/state/reducer.ts @@ -26,6 +26,15 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS }, }; + case "SET_TIMESTAMPS": + return { + ...state, + timestamps: { + ...state.timestamps, + ...action.payload, + }, + }; + case "ADD_ATTACHMENT": return { ...state, diff --git a/web/src/components/MemoEditor/state/types.ts b/web/src/components/MemoEditor/state/types.ts index 48289b210245d..8612729579b6c 100644 --- a/web/src/components/MemoEditor/state/types.ts +++ b/web/src/components/MemoEditor/state/types.ts @@ -34,6 +34,7 @@ export type EditorAction = | { type: "INIT_MEMO"; payload: { content: string; metadata: EditorState["metadata"]; timestamps: EditorState["timestamps"] } } | { type: "UPDATE_CONTENT"; payload: string } | { type: "SET_METADATA"; payload: Partial } + | { type: "SET_TIMESTAMPS"; payload: Partial } | { type: "ADD_ATTACHMENT"; payload: Attachment } | { type: "REMOVE_ATTACHMENT"; payload: string } | { type: "ADD_RELATION"; payload: MemoRelation } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 41d8974151428..2d46678891a6c 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -124,7 +124,8 @@ "no-changes-detected": "No changes detected", "focus-mode": "Focus Mode", "exit-focus-mode": "Exit Focus Mode", - "slash-commands": "Type `/` for commands" + "slash-commands": "Type `/` for commands", + "created-at": "Created at" }, "filters": { "has-code": "hasCode", From dcdf001b6f83cb72010debd302a17812ea8fbfdd Mon Sep 17 00:00:00 2001 From: Giulio <3272563+giuliohome@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:51:16 +0100 Subject: [PATCH 4/5] feat: Add debug logging for memo creation date --- .gitignore | 2 +- cmd/memos/main.go | 1 + server/router/api/v1/memo_service.go | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index abd9d94fcb5ee..2d8e5355562e5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ web/dist # Build artifacts build/ bin/ -memos + # Plan/design documents docs/plans/ diff --git a/cmd/memos/main.go b/cmd/memos/main.go index 48dad202d5fee..6ecf58a7ef28f 100644 --- a/cmd/memos/main.go +++ b/cmd/memos/main.go @@ -169,6 +169,7 @@ func printGreetings(profile *profile.Profile) { } func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))) if err := rootCmd.Execute(); err != nil { panic(err) } diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 88ccc971b74d7..b83ac6f74a1c6 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -45,15 +45,19 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR Content: request.Memo.Content, Visibility: convertVisibilityToStore(request.Memo.Visibility), } + slog.Debug("CreateMemo: request.Memo.CreateTime", "value", request.Memo.CreateTime) if request.Memo.CreateTime != nil { create.CreatedTs = request.Memo.CreateTime.AsTime().Unix() } + slog.Debug("CreateMemo: request.Memo.UpdateTime", "value", request.Memo.UpdateTime) // If UpdateTime is provided, use it. Otherwise, if CreateTime is provided, use it for UpdatedTs as well. if request.Memo.UpdateTime != nil { create.UpdatedTs = request.Memo.UpdateTime.AsTime().Unix() } else if request.Memo.CreateTime != nil { create.UpdatedTs = request.Memo.CreateTime.AsTime().Unix() } + slog.Debug("CreateMemo: store.Memo timestamps before calling s.Store.CreateMemo", + "createdTs", create.CreatedTs, "updatedTs", create.UpdatedTs) instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting") From e8215d9963be1aa08fae7a4eb1414117223f2514 Mon Sep 17 00:00:00 2001 From: Giulio <3272563+giuliohome@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:56:45 +0100 Subject: [PATCH 5/5] fix(memo): Honor frontend-provided creation date for new memos --- server/router/api/v1/memo_service.go | 4 ---- store/db/postgres/memo.go | 4 +--- store/db/sqlite/memo.go | 19 ++++++++++++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index b83ac6f74a1c6..88ccc971b74d7 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -45,19 +45,15 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR Content: request.Memo.Content, Visibility: convertVisibilityToStore(request.Memo.Visibility), } - slog.Debug("CreateMemo: request.Memo.CreateTime", "value", request.Memo.CreateTime) if request.Memo.CreateTime != nil { create.CreatedTs = request.Memo.CreateTime.AsTime().Unix() } - slog.Debug("CreateMemo: request.Memo.UpdateTime", "value", request.Memo.UpdateTime) // If UpdateTime is provided, use it. Otherwise, if CreateTime is provided, use it for UpdatedTs as well. if request.Memo.UpdateTime != nil { create.UpdatedTs = request.Memo.UpdateTime.AsTime().Unix() } else if request.Memo.CreateTime != nil { create.UpdatedTs = request.Memo.CreateTime.AsTime().Unix() } - slog.Debug("CreateMemo: store.Memo timestamps before calling s.Store.CreateMemo", - "createdTs", create.CreatedTs, "updatedTs", create.UpdatedTs) instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting") diff --git a/store/db/postgres/memo.go b/store/db/postgres/memo.go index ff416eac74dfe..f8dfe1c3b4f37 100644 --- a/store/db/postgres/memo.go +++ b/store/db/postgres/memo.go @@ -36,11 +36,9 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e fields = append(fields, "payload") args = append(args, payload) - stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts, updated_ts, row_status" + stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, row_status" if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( &create.ID, - &create.CreatedTs, - &create.UpdatedTs, &create.RowStatus, ); err != nil { return nil, err diff --git a/store/db/sqlite/memo.go b/store/db/sqlite/memo.go index f3bc2f54d17eb..8c9e6c234ae1a 100644 --- a/store/db/sqlite/memo.go +++ b/store/db/sqlite/memo.go @@ -16,6 +16,15 @@ import ( func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"} placeholder := []string{"?", "?", "?", "?", "?"} + if create.CreatedTs != 0 { + fields = append(fields, "`created_ts`") + placeholder = append(placeholder, "?") + } + if create.UpdatedTs != 0 { + fields = append(fields, "`updated_ts`") + placeholder = append(placeholder, "?") + } + payload := "{}" if create.Payload != nil { payloadBytes, err := protojson.Marshal(create.Payload) @@ -25,12 +34,16 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e payload = string(payloadBytes) } args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + if create.CreatedTs != 0 { + args = append(args, create.CreatedTs) + } + if create.UpdatedTs != 0 { + args = append(args, create.UpdatedTs) + } - stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`" + stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `row_status`" if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( &create.ID, - &create.CreatedTs, - &create.UpdatedTs, &create.RowStatus, ); err != nil { return nil, err