feat(kanban): calendar view (month grid + agenda)#35
Conversation
Add a third board view alongside Board/Table — a month grid and an agenda list keyed off each card's `due`. Pure presentational layer over the shared board model; no backend, migration, or WS changes. Because both the desktop file board and the web DB board render the shared BoardSurface, the calendar lands on both for free. - shared/lib/board.ts: widen BoardViewType with "calendar", add CalendarMode + calendarMode config field, and pure helpers isIsoDate / currentMonth / shiftMonth / groupCardsByDay / monthMatrix (zero-padded ISO strings, no date library — matches sortCards/todayStr). - shared/components/board/BoardCalendar.tsx: new month-grid + agenda component. Overdue (due < today && not in done column) shows red; today is highlighted; out-of-month and undated cards handled; card click opens the same BoardPeek. - BoardSurface: Calendar toggle button, third render branch, hide sort in calendar mode (group-by was already board-only); persist calendarMode via setConfig. - Thread calendar viewType + calendarMode through all three config types (desktop BoardConfig, web file-board BoardConfigJSON, web DB-board view). - Fix stale setConfig persistence comment in board/types.ts. - tests/unit/boardCalendar.spec.ts: cover the calendar helpers. - i18n: extract new shared strings, add zh translations, compile. Verified: both tsc projects clean, 9 unit tests pass, web build succeeds, and a throwaway harness render confirmed month grid, agenda, overdue styling, today highlight, mode toggle, and peek-on-click. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughA new Calendar view is added to the board system alongside the existing Board and Table views. It supports month-grid and agenda sub-modes, controlled via a new ChangesBoard Calendar View
Sequence DiagramsequenceDiagram
participant User
participant BoardSurface
participant BoardCalendar
participant board_ts as board.ts helpers
participant actions as setConfig
User->>BoardSurface: click "Calendar" in view switcher
BoardSurface->>actions: setConfig({ viewType: "calendar" })
BoardSurface->>BoardCalendar: render(cards, today, doneKey, calendarMode="month")
BoardCalendar->>board_ts: monthMatrix(currentMonth())
board_ts-->>BoardCalendar: 6×7 ISO day strings
BoardCalendar->>board_ts: groupCardsByDay(cards)
board_ts-->>BoardCalendar: Map<due, cards[]>
BoardCalendar-->>User: month grid with card chips
User->>BoardCalendar: toggle "Agenda" mode
BoardCalendar->>actions: setConfig({ calendarMode: "agenda" })
BoardCalendar->>board_ts: sort cards by due, split dated/undated
board_ts-->>BoardCalendar: sorted + grouped cards
BoardCalendar-->>User: agenda list with day headers and Unscheduled section
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
# Conflicts: # services/jtype-web/frontend/src/pages/Kanban.tsx # services/jtype-web/frontend/src/pages/WebBoardView.tsx # shared/components/board/BoardSurface.tsx # shared/components/board/index.ts # shared/i18n/locales/en/messages.mjs # shared/i18n/locales/en/messages.po # shared/i18n/locales/ja/messages.mjs # shared/i18n/locales/ja/messages.po # shared/i18n/locales/ko/messages.mjs # shared/i18n/locales/ko/messages.po # shared/i18n/locales/zh/messages.mjs # shared/i18n/locales/zh/messages.po # shared/lib/board.ts # src/components/BoardView.tsx # src/lib/types.ts
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
tests/unit/boardCalendar.spec.ts (1)
19-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd impossible-date cases to
isIsoDatetests.Please include calendar-invalid inputs (e.g.,
2026-02-31,2026-13-01) so the validation contract is enforced by tests.Suggested test additions
test("isIsoDate accepts only zero-padded YYYY-MM-DD", () => { expect(isIsoDate("2026-06-22")).toBe(true); expect(isIsoDate("2026-6-2")).toBe(false); // not zero-padded expect(isIsoDate("2026/06/22")).toBe(false); + expect(isIsoDate("2026-02-31")).toBe(false); // impossible day + expect(isIsoDate("2026-13-01")).toBe(false); // impossible month expect(isIsoDate("")).toBe(false); expect(isIsoDate(null)).toBe(false); expect(isIsoDate(undefined)).toBe(false); expect(isIsoDate("garbage")).toBe(false); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/boardCalendar.spec.ts` around lines 19 - 27, The test function isIsoDate needs additional test cases to validate that impossible calendar dates are rejected, not just format validation. Add test assertions within the existing test block that check edge cases like February 31st (2026-02-31) and invalid month numbers (2026-13-01) to ensure the validation logic properly rejects dates that are syntactically correct ISO format but semantically invalid on a calendar.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@shared/components/board/BoardCalendar.tsx`:
- Line 1: The BoardCalendar component uses useState on line 45 to manage month
cursor state internally, violating the Props-in/Callbacks-out pattern required
for shared components. Remove the useState hook for month cursor from
BoardCalendar and replace it with props passed from the parent component.
Convert the state mutations occurring at lines 90, 93, and 100 into callback
props (such as onMonthChange or similar) that invoke parent-provided handlers
instead of directly updating state. The parent component should be responsible
for managing the month cursor state and passing both the current value and
update callbacks as props to BoardCalendar.
In `@shared/i18n/locales/ja/messages.po`:
- Around line 63-66: The Japanese translation file contains empty msgstr values
for new translation entries, including the "Agenda" entry and others at the
specified line ranges (109-112, 305-317, 373-376, 499-502, 515-518), which will
cause these labels to fall back to English when displayed to Japanese users.
Fill in all empty msgstr values with appropriate Japanese translations for each
corresponding msgid entry before shipping, or remove these incomplete
translation entries from the file.
In `@shared/i18n/locales/ko/messages.po`:
- Around line 63-66: The Korean translation file (messages.po) contains empty
msgstr values for multiple translation entries including "Agenda" and others at
the specified line ranges. Fill in each empty msgstr field with the appropriate
Korean translation for the corresponding msgid. Ensure all empty msgstr entries
at lines 63-66, 109-112, 305-317, 373-376, 499-502, and 515-518 are populated
with valid Korean translations before releasing to prevent the UI from falling
back to English text in Korean locale.
In `@shared/lib/board.ts`:
- Around line 356-359: The isIsoDate function only validates the format of the
date string using a regex pattern, but does not check whether the date
represents a valid calendar date. This allows impossible dates like 2026-02-31
or 2026-13-01 to pass validation. Strengthen the isIsoDate function by parsing
the month and day components from the matched string and validating that they
represent valid calendar values (month between 1 and 12, day between 1 and the
maximum days for that month). Consider using JavaScript's Date object to
validate the actual date after confirming the format matches the regex pattern.
In `@src/lib/types.ts`:
- Around line 22-25: The BoardConfig definition is currently using the type
keyword, but per the frontend coding guidelines, object shapes should be
declared as interfaces instead. Convert the BoardConfig from a type declaration
to an interface declaration to ensure consistency with the typing policy for
object shape definitions.
---
Nitpick comments:
In `@tests/unit/boardCalendar.spec.ts`:
- Around line 19-27: The test function isIsoDate needs additional test cases to
validate that impossible calendar dates are rejected, not just format
validation. Add test assertions within the existing test block that check edge
cases like February 31st (2026-02-31) and invalid month numbers (2026-13-01) to
ensure the validation logic properly rejects dates that are syntactically
correct ISO format but semantically invalid on a calendar.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d896357c-4131-4ab5-99c0-61b676276b60
📒 Files selected for processing (18)
services/jtype-web/frontend/src/pages/Kanban.tsxservices/jtype-web/frontend/src/pages/WebBoardView.tsxshared/components/board/BoardCalendar.tsxshared/components/board/BoardSurface.tsxshared/components/board/index.tsshared/components/board/types.tsshared/i18n/locales/en/messages.mjsshared/i18n/locales/en/messages.poshared/i18n/locales/ja/messages.mjsshared/i18n/locales/ja/messages.poshared/i18n/locales/ko/messages.mjsshared/i18n/locales/ko/messages.poshared/i18n/locales/zh/messages.mjsshared/i18n/locales/zh/messages.poshared/lib/board.tssrc/components/BoardView.tsxsrc/lib/types.tstests/unit/boardCalendar.spec.ts
| @@ -0,0 +1,203 @@ | |||
| import { useState } from "react"; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
find . -path "*shared/components/board/BoardCalendar.tsx" -type fRepository: cnjack/jtype
Length of output: 194
🏁 Script executed:
if [ -f shared/components/board/BoardCalendar.tsx ]; then
wc -l shared/components/board/BoardCalendar.tsx
fiRepository: cnjack/jtype
Length of output: 196
🏁 Script executed:
cat -n shared/components/board/BoardCalendar.tsxRepository: cnjack/jtype
Length of output: 9469
Hoist month cursor state out of the shared component.
Line 45 introduces local useState, and Lines 90/93/100 mutate it internally. In shared/components, this should be controlled via props/callbacks so the component stays Props-in/Callbacks-out.
Suggested refactor
-import { useState } from "react";
import { t } from "`@lingui/core/macro`";
import { Trans } from "`@lingui/react/macro`";
@@
export function BoardCalendar({
cards,
today,
doneKey,
+ month,
+ onMonthChange,
mode,
onModeChange,
selectedId,
onSelect,
}: {
cards: BoardViewCard[];
today: string;
doneKey: string;
+ month: string;
+ onMonthChange: (month: string) => void;
mode: CalendarMode;
onModeChange: (mode: CalendarMode) => void;
selectedId?: string;
onSelect: (card: BoardViewCard) => void;
}) {
- const [month, setMonth] = useState(() => currentMonth());
const byDay = groupCardsByDay(cards);
@@
- <button type="button" className={navBtn} title={t`Previous month`} aria-label={t`Previous month`} onClick={() => setMonth((m) => shiftMonth(m, -1))}>
+ <button type="button" className={navBtn} title={t`Previous month`} aria-label={t`Previous month`} onClick={() => onMonthChange(shiftMonth(month, -1))}>
@@
- <button type="button" className={navBtn} title={t`Next month`} aria-label={t`Next month`} onClick={() => setMonth((m) => shiftMonth(m, 1))}>
+ <button type="button" className={navBtn} title={t`Next month`} aria-label={t`Next month`} onClick={() => onMonthChange(shiftMonth(month, 1))}>
@@
- onClick={() => setMonth(currentMonth())}
+ onClick={() => onMonthChange(currentMonth())}Per coding guidelines: "Shared React components in shared/ must follow Props-in/Callbacks-out pattern and must not depend on useReducer, useState, or any specific state pattern."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/components/board/BoardCalendar.tsx` at line 1, The BoardCalendar
component uses useState on line 45 to manage month cursor state internally,
violating the Props-in/Callbacks-out pattern required for shared components.
Remove the useState hook for month cursor from BoardCalendar and replace it with
props passed from the parent component. Convert the state mutations occurring at
lines 90, 93, and 100 into callback props (such as onMonthChange or similar)
that invoke parent-provided handlers instead of directly updating state. The
parent component should be responsible for managing the month cursor state and
passing both the current value and update callbacks as props to BoardCalendar.
Source: Coding guidelines
| #: shared/components/board/BoardCalendar.tsx | ||
| msgid "Agenda" | ||
| msgstr "" | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fill the new Japanese translations before shipping.
All added msgstr values are empty, so the new Calendar/Agenda labels will fall back to English in Japanese. Please populate these entries or keep the locale update out until they’re ready.
Also applies to: 109-112, 305-317, 373-376, 499-502, 515-518
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/i18n/locales/ja/messages.po` around lines 63 - 66, The Japanese
translation file contains empty msgstr values for new translation entries,
including the "Agenda" entry and others at the specified line ranges (109-112,
305-317, 373-376, 499-502, 515-518), which will cause these labels to fall back
to English when displayed to Japanese users. Fill in all empty msgstr values
with appropriate Japanese translations for each corresponding msgid entry before
shipping, or remove these incomplete translation entries from the file.
| #: shared/components/board/BoardCalendar.tsx | ||
| msgid "Agenda" | ||
| msgstr "" | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fill the new Korean translations before shipping.
All added msgstr values are empty, so the new Calendar/Agenda labels will fall back to English in Korean, and the generated runtime bundle will inherit the same fallback. Please populate these entries before release.
Also applies to: 109-112, 305-317, 373-376, 499-502, 515-518
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/i18n/locales/ko/messages.po` around lines 63 - 66, The Korean
translation file (messages.po) contains empty msgstr values for multiple
translation entries including "Agenda" and others at the specified line ranges.
Fill in each empty msgstr field with the appropriate Korean translation for the
corresponding msgid. Ensure all empty msgstr entries at lines 63-66, 109-112,
305-317, 373-376, 499-502, and 515-518 are populated with valid Korean
translations before releasing to prevent the UI from falling back to English
text in Korean locale.
| /** True when `s` is a well-formed zero-padded ISO date (`YYYY-MM-DD`). */ | ||
| export function isIsoDate(s: string | null | undefined): s is string { | ||
| return !!s && /^\d{4}-\d{2}-\d{2}$/.test(s); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Strengthen isIsoDate to reject impossible calendar dates.
The regex-only check accepts values like 2026-02-31 and 2026-13-01, so invalid due values can still be bucketed as scheduled days.
Suggested fix
export function isIsoDate(s: string | null | undefined): s is string {
- return !!s && /^\d{4}-\d{2}-\d{2}$/.test(s);
+ if (!s || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
+ const [ys, ms, ds] = s.split("-");
+ const y = Number(ys);
+ const m = Number(ms);
+ const d = Number(ds);
+ const parsed = new Date(y, m - 1, d);
+ return parsed.getFullYear() === y && parsed.getMonth() === m - 1 && parsed.getDate() === d;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/lib/board.ts` around lines 356 - 359, The isIsoDate function only
validates the format of the date string using a regex pattern, but does not
check whether the date represents a valid calendar date. This allows impossible
dates like 2026-02-31 or 2026-13-01 to pass validation. Strengthen the isIsoDate
function by parsing the month and day components from the matched string and
validating that they represent valid calendar values (month between 1 and 12,
day between 1 and the maximum days for that month). Consider using JavaScript's
Date object to validate the actual date after confirming the format matches the
regex pattern.
| /** Which renderer this board shows: kanban columns, a flat table, or a calendar. Defaults to "board". */ | ||
| viewType?: "board" | "table" | "calendar"; | ||
| /** Calendar sub-mode (month grid vs agenda list) when viewType === "calendar". Defaults to "month". */ | ||
| calendarMode?: "month" | "agenda"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify object-shape type aliases under src frontend code.
rg -nP --iglob 'src/**/*.{ts,tsx}' '^\s*export\s+type\s+\w+\s*=\s*\{'Repository: cnjack/jtype
Length of output: 150
🏁 Script executed:
cat -n src/lib/types.ts | head -50Repository: cnjack/jtype
Length of output: 2391
Use interface for BoardConfig to match frontend typing policy.
BoardConfig should be declared as an interface per the coding guideline "Use interface for defining object shapes in TypeScript frontend code".
Suggested fix
-export type BoardConfig = {
+export interface BoardConfig {
id: string;
title: string;
groupBy: string;
columns: BoardColumn[];
/** Column key treated as terminal/done (suppresses overdue styling). Defaults to "done". */
doneColumn?: string;
/** When true, tint each column header by its column color. */
colorColumns?: boolean;
/** Which renderer this board shows: kanban columns, a flat table, or a calendar. Defaults to "board". */
viewType?: "board" | "table" | "calendar";
/** Calendar sub-mode (month grid vs agenda list) when viewType === "calendar". Defaults to "month". */
calendarMode?: "month" | "agenda";
/** User-defined custom fields shown/edited on this board's cards. */
fields?: { key: string; label: string; type?: "text" | "number" | "date" }[];
/** Optional second grouping dimension rendered as swimlane rows in the board view. */
swimlaneBy?: "status" | "priority" | "assignee";
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Which renderer this board shows: kanban columns, a flat table, or a calendar. Defaults to "board". */ | |
| viewType?: "board" | "table" | "calendar"; | |
| /** Calendar sub-mode (month grid vs agenda list) when viewType === "calendar". Defaults to "month". */ | |
| calendarMode?: "month" | "agenda"; | |
| export interface BoardConfig { | |
| id: string; | |
| title: string; | |
| groupBy: string; | |
| columns: BoardColumn[]; | |
| /** Column key treated as terminal/done (suppresses overdue styling). Defaults to "done". */ | |
| doneColumn?: string; | |
| /** When true, tint each column header by its column color. */ | |
| colorColumns?: boolean; | |
| /** Which renderer this board shows: kanban columns, a flat table, or a calendar. Defaults to "board". */ | |
| viewType?: "board" | "table" | "calendar"; | |
| /** Calendar sub-mode (month grid vs agenda list) when viewType === "calendar". Defaults to "month". */ | |
| calendarMode?: "month" | "agenda"; | |
| /** User-defined custom fields shown/edited on this board's cards. */ | |
| fields?: { key: string; label: string; type?: "text" | "number" | "date" }[]; | |
| /** Optional second grouping dimension rendered as swimlane rows in the board view. */ | |
| swimlaneBy?: "status" | "priority" | "assignee"; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/types.ts` around lines 22 - 25, The BoardConfig definition is
currently using the type keyword, but per the frontend coding guidelines, object
shapes should be declared as interfaces instead. Convert the BoardConfig from a
type declaration to an interface declaration to ensure consistency with the
typing policy for object shape definitions.
Source: Coding guidelines
What
Adds a third board view alongside Board and Table — a month grid and an agenda list, keyed off each card's
duedate. Implements C3 from the kanban roadmap (design §2).Because both the desktop file board and the web DB board render the shared
BoardSurface, the calendar lands on both platforms for free.Scope
Pure presentational layer over the existing shared board model — no backend, no migration, no WebSocket changes. All data (
due) already rides the document-sync pipeline / DB; the remembered sub-mode (calendarMode) persists in the board config likeviewType.Changes
shared/lib/board.ts— widenBoardViewTypewith"calendar", addCalendarMode+calendarMode, and pure helpersisIsoDate/currentMonth/shiftMonth/groupCardsByDay/monthMatrix(zero-padded ISO strings, no date library — consistent withsortCards/todayStr).shared/components/board/BoardCalendar.tsx(new) — month grid + agenda. Overdue (due < todayand not in the done column) renders red; today is highlighted; out-of-month and undated cards are handled; a card click opens the sameBoardPeek(so due editing reuses the existing date input — no new edit UI).BoardSurface— Calendar toggle, third render branch, hide the sort dropdown in calendar mode (group-by was already board-only).viewType: "calendar"+calendarModethrough all three config types (desktopBoardConfig, web file-boardBoardConfigJSON, web DB-board view).setConfigpersistence comment inboard/types.ts.tests/unit/boardCalendar.spec.ts— cover the calendar helpers (6 tests).Verification
tsc --noEmitclean for both the root (desktop+shared) and web-frontend projects.pnpm test:unit).vite buildsucceeds.setConfig), and card-click →BoardPeekwith the Due input.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores