memos/web/src/components/StatisticsView/StatisticsView.tsx
Steven 8daef1dc89 feat(activity-calendar): aggregate by ViewContext.timeBasis
Fixes the inconsistency where switching the memo list to update_time
left the activity heatmap aggregating by created_time. The heatmap
now follows the same time basis as the list it sits next to.

Backend
- UserStats gains memo_updated_timestamps (additive proto field, tag 8).
- GetUserStats and ListAllUserStats populate it alongside the existing
  memo_created_timestamps. No DB migration; memo.updated_ts already
  exists on every row.

Frontend
- useFilteredMemoStats reads timeBasis from ViewContext and selects
  the matching timestamp source.
- StatisticsView and MonthNavigator forward timeBasis through to
  MonthCalendar / YearCalendar so tooltip text matches the basis
  ("X memos in DATE" vs "X memos updated on DATE").
- Falls back to memoCreatedTimestamps when an old server returns an
  empty memoUpdatedTimestamps array (detected by length divergence,
  since protobuf-es deserializes missing repeated fields as []).

Tests
- Backend: TestGetUserStats_MemoUpdatedTimestamps verifies the field
  is populated and reflects post-creation updates.
- Frontend: filtered-memo-stats covers create/update source switching
  and the old-server fallback path; activity-calendar-tooltip covers
  basis-aware label selection.

Spec and implementation plan committed under docs/superpowers/.
2026-05-02 00:26:53 +08:00

40 lines
1.2 KiB
TypeScript

import dayjs from "dayjs";
import { useState } from "react";
import { calculateMaxCount, MonthCalendar } from "@/components/ActivityCalendar";
import { useDateFilterNavigation } from "@/hooks";
import type { StatisticsData } from "@/types/statistics";
import { MonthNavigator } from "./MonthNavigator";
interface Props {
statisticsData: StatisticsData;
}
const StatisticsView = (props: Props) => {
const { statisticsData } = props;
const { activityStats, timeBasis } = statisticsData;
const navigateToDateFilter = useDateFilterNavigation();
const [visibleMonthString, setVisibleMonthString] = useState(dayjs().format("YYYY-MM"));
return (
<div className="group w-full mt-2 flex flex-col text-muted-foreground animate-fade-in">
<MonthNavigator
visibleMonth={visibleMonthString}
onMonthChange={setVisibleMonthString}
activityStats={activityStats}
timeBasis={timeBasis}
/>
<div className="w-full animate-scale-in">
<MonthCalendar
month={visibleMonthString}
data={activityStats}
maxCount={calculateMaxCount(activityStats)}
onClick={navigateToDateFilter}
timeBasis={timeBasis}
/>
</div>
</div>
);
};
export default StatisticsView;