✨ Add Content Calendar view with filter support#26430
✨ Add Content Calendar view with filter support#26430brandenwagner wants to merge 12 commits intoTryGhost:mainfrom
Conversation
ref https://github.com/TryGhost/Ghost Podman compose split the SQL healthcheck command argument and caused repeated "Unknown database '1'" failures, delaying ghost-dev startup readiness.
ref https://github.com/TryGhost/Ghost Added a posts calendar route, view, and date utilities, plus unit tests and sidebar navigation so calendar pages are accessible from Admin.
WalkthroughAdds a calendar view to the Posts app: extends 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`:
- Around line 321-327: The query currently passes limit: 'all' to useBrowsePosts
causing it to fetch every post matching calendarFilter; change the call to
constrain results by adding a date-range filter (e.g., startDate and endDate
spanning the visible month ± buffer) to the searchParams.filter before calling
useBrowsePosts so it only fetches posts for the calendar view; locate where
useBrowsePosts is invoked and where calendarFilter/calendarOrder are composed
and merge the computed dateRange into calendarFilter (or replace limit: 'all'
with a paginated/limited query) so the request returns only the relevant month’s
posts.
- Around line 448-487: Import the formatMonthLabel helper from the utils at the
top of the file and render a small, read-only month label between the navigation
buttons using the current month offset state (the same state modified by
setMonthOffset). Specifically, add an import for formatMonthLabel and insert a
span/div between the "Today" button and the Chevron buttons that calls
formatMonthLabel(monthOffset) (or the component's current offset variable) to
display the month/year string so users can see which month is shown.
- Around line 26-82: Add a short TODO comment above the LegacyFilterSelect
component explaining it’s a temporary compatibility component and describing the
intended migration path (e.g., replace LegacyFilterSelect with the Shade Select
component and remove ember-* CSS classes once the legacy admin is retired),
include any relevant tracking issue or ticket ID if available, and note why the
ember-* classes are currently preserved for visual parity; reference the
LegacyFilterSelect function/component name and the ember-* CSS classes (e.g.,
'ember-view', 'ember-basic-dropdown-trigger') so future contributors can find
and remove this temporary code during migration.
In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`:
- Around line 72-78: getTimestamp can return NaN for malformed date strings
which breaks sorting; update the getTimestamp function to validate the parsed
time (from new Date(value).getTime() or Date.parse) and return a safe fallback
(e.g., 0 or a clearly defined sentinel) when the parsed result is NaN or value
is invalid so comparisons remain stable—modify the body of getTimestamp to parse
the date, check isNaN(parsed) and return the fallback on failure, otherwise
return the parsed timestamp.
In `@apps/posts/test/unit/utils/content-calendar.test.ts`:
- Around line 64-76: Add a unit test that verifies the priority chain updated_at
→ created_at for draft posts: create a draft post via createPost with both
updated_at and created_at set to different dates, call buildCalendarGrid with
that post (same month/timeZone as existing tests), find the day by dateKey for
the updated_at date, and assert the grid places the post on the updated_at date
(showing updated_at wins over created_at).
🧹 Nitpick comments (3)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`: - Around line 26-82: Add a short TODO comment above the LegacyFilterSelect component explaining it’s a temporary compatibility component and describing the intended migration path (e.g., replace LegacyFilterSelect with the Shade Select component and remove ember-* CSS classes once the legacy admin is retired), include any relevant tracking issue or ticket ID if available, and note why the ember-* classes are currently preserved for visual parity; reference the LegacyFilterSelect function/component name and the ember-* CSS classes (e.g., 'ember-view', 'ember-basic-dropdown-trigger') so future contributors can find and remove this temporary code during migration. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`: - Around line 72-78: getTimestamp can return NaN for malformed date strings which breaks sorting; update the getTimestamp function to validate the parsed time (from new Date(value).getTime() or Date.parse) and return a safe fallback (e.g., 0 or a clearly defined sentinel) when the parsed result is NaN or value is invalid so comparisons remain stable—modify the body of getTimestamp to parse the date, check isNaN(parsed) and return the fallback on failure, otherwise return the parsed timestamp. In `@apps/posts/test/unit/utils/content-calendar.test.ts`: - Around line 64-76: Add a unit test that verifies the priority chain updated_at → created_at for draft posts: create a draft post via createPost with both updated_at and created_at set to different dates, call buildCalendarGrid with that post (same month/timeZone as existing tests), find the day by dateKey for the updated_at date, and assert the grid places the post on the updated_at date (showing updated_at wins over created_at).apps/posts/test/unit/utils/content-calendar.test.ts (1)
64-76: Consider adding a test for the fullupdated_at → created_atpriority chain for drafts.The current tests cover a draft with only
updated_at(line 34) and one with onlycreated_at(line 69), but there's no case where both are present to verifyupdated_atwins. A quick additional test would harden the fallback contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/test/unit/utils/content-calendar.test.ts` around lines 64 - 76, Add a unit test that verifies the priority chain updated_at → created_at for draft posts: create a draft post via createPost with both updated_at and created_at set to different dates, call buildCalendarGrid with that post (same month/timeZone as existing tests), find the day by dateKey for the updated_at date, and assert the grid places the post on the updated_at date (showing updated_at wins over created_at).apps/posts/src/views/ContentCalendar/content-calendar.tsx (1)
26-82:LegacyFilterSelect— naming hints at technical debt; consider a TODO or plan to migrate.The component name and its use of
ember-*CSS classes make it clear this is a stopgap for visual parity with the Ember admin. This is fine for now, but a brief comment about the intended migration path (e.g., replacing with a Shade<Select>component once the legacy admin is retired) would help future contributors.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx` around lines 26 - 82, Add a short TODO comment above the LegacyFilterSelect component explaining it’s a temporary compatibility component and describing the intended migration path (e.g., replace LegacyFilterSelect with the Shade Select component and remove ember-* CSS classes once the legacy admin is retired), include any relevant tracking issue or ticket ID if available, and note why the ember-* classes are currently preserved for visual parity; reference the LegacyFilterSelect function/component name and the ember-* CSS classes (e.g., 'ember-view', 'ember-basic-dropdown-trigger') so future contributors can find and remove this temporary code during migration.apps/posts/src/views/ContentCalendar/utils/calendar.ts (1)
72-78:getTimestampcan returnNaNfor malformed date strings, breaking sort stability.If
valueis a non-empty but invalid date string,new Date(value).getTime()returnsNaN. Comparisons involvingNaNalways evaluate tofalse, which can produce nondeterministic sort results.Low risk since values come from the API, but a one-line guard makes this bulletproof.
Proposed fix
const getTimestamp = (value?: string) => { if (!value) { return 0; } - return new Date(value).getTime(); + const ts = new Date(value).getTime(); + return Number.isNaN(ts) ? 0 : ts; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts` around lines 72 - 78, getTimestamp can return NaN for malformed date strings which breaks sorting; update the getTimestamp function to validate the parsed time (from new Date(value).getTime() or Date.parse) and return a safe fallback (e.g., 0 or a clearly defined sentinel) when the parsed result is NaN or value is invalid so comparisons remain stable—modify the body of getTimestamp to parse the date, check isNaN(parsed) and return the fallback on failure, otherwise return the parsed timestamp.
ref TryGhost#26430 Reduced content-calendar fetch size by applying a month-window date filter and added an inline month/year label in navigation controls.
ref TryGhost#26430 Documented that LegacyFilterSelect and ember-* classes are temporary and should be removed when the app fully moves to Shade-native controls.
ref TryGhost#26430 Guarded getTimestamp against invalid date strings so sort comparisons stay deterministic when malformed values are present.
ref TryGhost#26430 Covered the case where draft posts have both updated_at and created_at to verify updated_at takes precedence in calendar placement.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`:
- Around line 144-153: The query range currently ends at the last moment of
month.month+1 (March 31) which misses trailing days shown in a 6-week calendar;
update getCalendarDateRangeFilter to compute rangeEnd as the last displayed day
of the calendar grid (i.e., the last day of month.month + 1 in 1-based terms)
and set it to that day's end-of-day timestamp (23:59:59.999) so
published_at/updated_at/created_at filters include trailing grid cells; adjust
the rangeEnd calculation (and keep rangeStart as-is) inside
getCalendarDateRangeFilter so the returned filter string uses the new rangeEnd.
- Around line 131-153: The two functions getLegendStatusesForType and
getCalendarDateRangeFilter are duplicated; remove the local definitions in
content-calendar.tsx and instead import these functions from the shared calendar
utils module where they already exist (ensure that module exports
getLegendStatusesForType and getCalendarDateRangeFilter), update any local
references to use the imported symbols, and delete the duplicate implementations
so there is a single source of truth.
- Around line 220-354: ContentCalendar is too large—extract the calendar grid
rendering, filter bar, and month navigation into smaller subcomponents to
improve readability and testability: create CalendarGrid (props: calendarDays,
siteTimezone, month, calendarOrder, posts), FilterBar (props: authorOptions,
tagOptions, legendStatuses, hasActiveFilters, selectedType, selectedVisibility,
selectedAuthor, selectedTag, selectedOrder, setFilterParam, clearFilters) and
MonthNavigation (props: monthLabel, monthOffset, setMonthOffset) and replace the
inline JSX with these components inside ContentCalendar while keeping existing
hooks and helper functions (useBrowsePosts, buildCalendarGrid,
buildCalendarFilter, getCalendarDateRangeFilter) intact so state and
search-param handlers (setFilterParam, clearFilters) continue to work.
In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`:
- Around line 55-60: getDateKeyInTimezone and formatPostTime must guard against
invalid dateInput before calling getDatePartsInTimezone or Intl APIs; add a
check like const date = new Date(dateInput); if (isNaN(date.getTime())) throw
new RangeError(`Invalid dateInput passed to getDateKeyInTimezone`); (and the
same guard in formatPostTime) so you avoid
Intl.DateTimeFormat.format/formatToParts throwing unexpectedly; use the same
validation approach as getTimestamp and include a clear error message mentioning
the function name.
🧹 Nitpick comments (3)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`: - Around line 131-153: The two functions getLegendStatusesForType and getCalendarDateRangeFilter are duplicated; remove the local definitions in content-calendar.tsx and instead import these functions from the shared calendar utils module where they already exist (ensure that module exports getLegendStatusesForType and getCalendarDateRangeFilter), update any local references to use the imported symbols, and delete the duplicate implementations so there is a single source of truth. - Around line 220-354: ContentCalendar is too large—extract the calendar grid rendering, filter bar, and month navigation into smaller subcomponents to improve readability and testability: create CalendarGrid (props: calendarDays, siteTimezone, month, calendarOrder, posts), FilterBar (props: authorOptions, tagOptions, legendStatuses, hasActiveFilters, selectedType, selectedVisibility, selectedAuthor, selectedTag, selectedOrder, setFilterParam, clearFilters) and MonthNavigation (props: monthLabel, monthOffset, setMonthOffset) and replace the inline JSX with these components inside ContentCalendar while keeping existing hooks and helper functions (useBrowsePosts, buildCalendarGrid, buildCalendarFilter, getCalendarDateRangeFilter) intact so state and search-param handlers (setFilterParam, clearFilters) continue to work. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`: - Around line 55-60: getDateKeyInTimezone and formatPostTime must guard against invalid dateInput before calling getDatePartsInTimezone or Intl APIs; add a check like const date = new Date(dateInput); if (isNaN(date.getTime())) throw new RangeError(`Invalid dateInput passed to getDateKeyInTimezone`); (and the same guard in formatPostTime) so you avoid Intl.DateTimeFormat.format/formatToParts throwing unexpectedly; use the same validation approach as getTimestamp and include a clear error message mentioning the function name.apps/posts/src/views/ContentCalendar/utils/calendar.ts (1)
55-60: No guard against invaliddateInput—Intl.DateTimeFormat.format()throwsRangeErroronInvalid Date.
getTimestamp(line 72) already guards against invalid dates, butgetDateKeyInTimezoneandformatPostTimedo not. If an unexpected value slips through (e.g., an empty string or malformed ISO string),new Date(dateInput)producesInvalid DateandformatToParts/formatwill throw aRangeErrorat runtime.mapPostsByDayfilters out posts with nooccursAt, so the risk is low in the current call path, but the public export surface is unprotected.Proposed guard
export const getDateKeyInTimezone = (dateInput: string, timeZone: string) => { const date = new Date(dateInput); + if (Number.isNaN(date.getTime())) { + return ''; + } const {year, month, day} = getDatePartsInTimezone(date, timeZone);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts` around lines 55 - 60, getDateKeyInTimezone and formatPostTime must guard against invalid dateInput before calling getDatePartsInTimezone or Intl APIs; add a check like const date = new Date(dateInput); if (isNaN(date.getTime())) throw new RangeError(`Invalid dateInput passed to getDateKeyInTimezone`); (and the same guard in formatPostTime) so you avoid Intl.DateTimeFormat.format/formatToParts throwing unexpectedly; use the same validation approach as getTimestamp and include a clear error message mentioning the function name.apps/posts/src/views/ContentCalendar/content-calendar.tsx (2)
131-153:getLegendStatusesForTypeandgetCalendarDateRangeFilterappear duplicated inapps/posts/src/utils/calendar.ts.The relevant code snippets show identical functions at
apps/posts/src/utils/calendar.tslines 61–69. If that file exists alongside this one, these should be imported from a single shared module rather than defined in two places.#!/bin/bash # Verify if these functions exist in both locations rg -n 'getLegendStatusesForType|getCalendarDateRangeFilter' apps/posts/src --type ts -g '!*.test.*'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx` around lines 131 - 153, The two functions getLegendStatusesForType and getCalendarDateRangeFilter are duplicated; remove the local definitions in content-calendar.tsx and instead import these functions from the shared calendar utils module where they already exist (ensure that module exports getLegendStatusesForType and getCalendarDateRangeFilter), update any local references to use the imported symbols, and delete the duplicate implementations so there is a single source of truth.
220-354: Consider extracting sub-components to reduce component size.The
ContentCalendarcomponent is ~370 lines long. The calendar grid rendering (lines 528–579), filter bar (lines 356–423), and month navigation (lines 467–509) are natural extraction candidates. This would improve readability and testability.Not urgent for a first iteration, but worth noting for follow-up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx` around lines 220 - 354, ContentCalendar is too large—extract the calendar grid rendering, filter bar, and month navigation into smaller subcomponents to improve readability and testability: create CalendarGrid (props: calendarDays, siteTimezone, month, calendarOrder, posts), FilterBar (props: authorOptions, tagOptions, legendStatuses, hasActiveFilters, selectedType, selectedVisibility, selectedAuthor, selectedTag, selectedOrder, setFilterParam, clearFilters) and MonthNavigation (props: monthLabel, monthOffset, setMonthOffset) and replace the inline JSX with these components inside ContentCalendar while keeping existing hooks and helper functions (useBrowsePosts, buildCalendarGrid, buildCalendarFilter, getCalendarDateRangeFilter) intact so state and search-param handlers (setFilterParam, clearFilters) continue to work.
| const getCalendarDateRangeFilter = (month: CalendarMonth): string => { | ||
| const rangeStart = new Date(Date.UTC(month.year, month.month - 2, 1, 0, 0, 0, 0)).toISOString(); | ||
| const rangeEnd = new Date(Date.UTC(month.year, month.month + 1, 0, 23, 59, 59, 999)).toISOString(); | ||
|
|
||
| const rangeForField = (field: string) => { | ||
| return `${field}:>='${rangeStart}'+${field}:<='${rangeEnd}'`; | ||
| }; | ||
|
|
||
| return `(${rangeForField('published_at')},${rangeForField('updated_at')},${rangeForField('created_at')})`; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the date range calculation for February 2026
node -e "
const month = {year: 2026, month: 2}; // February, 1-based
const rangeStart = new Date(Date.UTC(month.year, month.month - 2, 1, 0, 0, 0, 0));
const rangeEnd = new Date(Date.UTC(month.year, month.month + 1, 0, 23, 59, 59, 999));
console.log('=== Query Date Range ===');
console.log('Range start:', rangeStart.toISOString());
console.log('Range end:', rangeEnd.toISOString());
// Check the calendar grid for February 2026
console.log('\n=== Calendar Grid Analysis ===');
const firstDay = new Date(Date.UTC(month.year, month.month - 1, 1));
const firstDayOfWeek = firstDay.getUTCDay(); // 0 = Sunday
console.log('Feb 1, 2026 is:', ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][firstDayOfWeek]);
console.log('First day of week index:', firstDayOfWeek);
const daysInMonth = new Date(Date.UTC(month.year, month.month, 0)).getUTCDate();
console.log('Days in Feb 2026:', daysInMonth);
// Calculate grid span: 6 weeks = 42 days
const gridSize = 42;
const totalDaysNeeded = firstDayOfWeek + daysInMonth;
const trailingDays = gridSize - totalDaysNeeded;
console.log('Days from previous month (grid padding):', firstDayOfWeek);
console.log('Days of current month:', daysInMonth);
console.log('Days from next month (trailing):', trailingDays);
console.log('\nGrid starts:', new Date(Date.UTC(month.year, month.month - 1, 1 - firstDayOfWeek)).toISOString().split('T')[0]);
console.log('Grid ends:', new Date(Date.UTC(month.year, month.month, daysInMonth + trailingDays)).toISOString().split('T')[0]);
"Repository: TryGhost/Ghost
Length of output: 408
Extend rangeEnd to cover trailing next-month days shown in the calendar grid.
For February 2026, the calendar grid spans Feb 1–28 (starting Sunday with no padding) plus 14 trailing days into March, but extends visually to April 11 in a 6-week calendar layout. The current query range ends March 31, leaving April 1–11 cells empty even if posts exist on those dates.
The fix should extend rangeEnd to the last displayed day of the grid:
Proposed fix
const getCalendarDateRangeFilter = (month: CalendarMonth): string => {
const rangeStart = new Date(Date.UTC(month.year, month.month - 2, 1, 0, 0, 0, 0)).toISOString();
- const rangeEnd = new Date(Date.UTC(month.year, month.month + 1, 0, 23, 59, 59, 999)).toISOString();
+ const rangeEnd = new Date(Date.UTC(month.year, month.month + 2, 0, 23, 59, 59, 999)).toISOString();This shifts rangeEnd to the last day of month month.month + 1 (1-based), ensuring trailing grid cells are included.
📝 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.
| const getCalendarDateRangeFilter = (month: CalendarMonth): string => { | |
| const rangeStart = new Date(Date.UTC(month.year, month.month - 2, 1, 0, 0, 0, 0)).toISOString(); | |
| const rangeEnd = new Date(Date.UTC(month.year, month.month + 1, 0, 23, 59, 59, 999)).toISOString(); | |
| const rangeForField = (field: string) => { | |
| return `${field}:>='${rangeStart}'+${field}:<='${rangeEnd}'`; | |
| }; | |
| return `(${rangeForField('published_at')},${rangeForField('updated_at')},${rangeForField('created_at')})`; | |
| }; | |
| const getCalendarDateRangeFilter = (month: CalendarMonth): string => { | |
| const rangeStart = new Date(Date.UTC(month.year, month.month - 2, 1, 0, 0, 0, 0)).toISOString(); | |
| const rangeEnd = new Date(Date.UTC(month.year, month.month + 2, 0, 23, 59, 59, 999)).toISOString(); | |
| const rangeForField = (field: string) => { | |
| return `${field}:>='${rangeStart}'+${field}:<='${rangeEnd}'`; | |
| }; | |
| return `(${rangeForField('published_at')},${rangeForField('updated_at')},${rangeForField('created_at')})`; | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx` around lines 144 -
153, The query range currently ends at the last moment of month.month+1 (March
31) which misses trailing days shown in a 6-week calendar; update
getCalendarDateRangeFilter to compute rangeEnd as the last displayed day of the
calendar grid (i.e., the last day of month.month + 1 in 1-based terms) and set
it to that day's end-of-day timestamp (23:59:59.999) so
published_at/updated_at/created_at filters include trailing grid cells; adjust
the rangeEnd calculation (and keep rangeStart as-is) inside
getCalendarDateRangeFilter so the returned filter string uses the new rangeEnd.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/posts/src/views/ContentCalendar/utils/calendar.ts (1)
106-121:sortCalendarPostsmutates the input array via.sort().Currently safe because the only call site passes a freshly-created
.filter()result, but the function signature suggests a pure transform. A stale-reference bug could surface if a future caller passes a shared array.Proposed fix — sort a shallow copy
const sortCalendarPosts = (posts: CalendarPost[], order: CalendarPostOrder): CalendarPost[] => { - return posts.sort((a, b) => { + return [...posts].sort((a, b) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts` around lines 106 - 121, sortCalendarPosts currently mutates its input by calling posts.sort(); change it to sort a shallow copy instead so the original array isn't mutated: create a copy of the posts array (e.g., via [...posts] or posts.slice()) and call .sort() on that copy before returning it; update the function around the posts parameter and the sort call in sortCalendarPosts to operate on the copied array so callers receive a new sorted array without modifying the input.apps/posts/src/views/ContentCalendar/content-calendar.tsx (1)
371-377: Add afieldsparameter to optimize the query payload.The query fetches full post objects but the calendar only consumes
id,title,status,published_at,updated_at, andcreated_at. The Ghost Admin API supports afieldsparameter that can meaningfully reduce payload size, especially withlimit: 'all'.Proposed change
const {data, isError, isLoading} = useBrowsePosts({ searchParams: { filter: postQueryFilter, limit: 'all', - order: calendarOrder + order: calendarOrder, + fields: 'id,title,status,published_at,updated_at,created_at' } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx` around lines 371 - 377, The browse posts query is fetching full post objects but the calendar only needs a small subset; update the call to useBrowsePosts to include a searchParams.fields parameter listing "id,title,status,published_at,updated_at,created_at" (comma-separated string) alongside the existing filter/limit/order (keep postQueryFilter and calendarOrder intact) so the request payload is smaller when limit: 'all'. Ensure the fields key is added to the same searchParams object passed into useBrowsePosts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`:
- Around line 163-172: Remove the leftover duplicate review marker and/or
redundant comment in the getCalendarDateRangeFilter implementation: delete the
stray "[duplicate_comment]" note and any duplicated explanatory text surrounding
the function so the code and its comment are not repeated; keep the existing
date-range logic (rangeStart/rangeEnd and rangeForField) intact in
getCalendarDateRangeFilter and ensure only a single, clear comment or none
remains.
---
Nitpick comments:
In `@apps/posts/src/views/ContentCalendar/content-calendar.tsx`:
- Around line 371-377: The browse posts query is fetching full post objects but
the calendar only needs a small subset; update the call to useBrowsePosts to
include a searchParams.fields parameter listing
"id,title,status,published_at,updated_at,created_at" (comma-separated string)
alongside the existing filter/limit/order (keep postQueryFilter and
calendarOrder intact) so the request payload is smaller when limit: 'all'.
Ensure the fields key is added to the same searchParams object passed into
useBrowsePosts.
In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`:
- Around line 106-121: sortCalendarPosts currently mutates its input by calling
posts.sort(); change it to sort a shallow copy instead so the original array
isn't mutated: create a copy of the posts array (e.g., via [...posts] or
posts.slice()) and call .sort() on that copy before returning it; update the
function around the posts parameter and the sort call in sortCalendarPosts to
operate on the copied array so callers receive a new sorted array without
modifying the input.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/posts/src/views/ContentCalendar/utils/calendar.ts (1)
142-157:sortCalendarPostsmutates its input array via.sort().Currently safe because
mapPostsByDaypasses a.filter()-produced copy, but this is a subtle coupling. If a future caller passes an array directly, it'll be mutated unexpectedly. Consider using.toSorted()or spreading first.♻️ Suggested change
const sortCalendarPosts = (posts: CalendarPost[], order: CalendarPostOrder): CalendarPost[] => { - return posts.sort((a, b) => { + return [...posts].sort((a, b) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts` around lines 142 - 157, sortCalendarPosts currently calls posts.sort(...) which mutates the input array; change it to return a new sorted array instead (e.g., use posts.toSorted(...) if available or return [...posts].sort(...)) so callers aren't surprised by in-place mutation. Update the implementation inside the sortCalendarPosts function to operate on a non-mutating copy and preserve the existing comparison logic for 'updated_at desc', 'published_at asc', and the default branch.apps/posts/test/unit/utils/content-calendar.test.ts (1)
56-70: Consider adding a test for'updated_at desc'ordering.You cover
published_at desc(default) andpublished_at asc, butupdated_at desc— the thirdCalendarPostOrdervariant — has no test. It has distinct logic (falls back tooccursAtwhenupdatedAtis absent).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/posts/test/unit/utils/content-calendar.test.ts` around lines 56 - 70, Add a unit test that verifies ordering when order is 'updated_at desc' by calling buildCalendarGrid with order: 'updated_at desc' and posts where one post has updated_at and another lacks updated_at so the logic falls back to occursAt; use createPost to craft posts (e.g., one with updated_at later and one without updated_at but different occursAt) locate the target day via grid.find(day => day.dateKey === 'YYYY-MM-DD') and assert the returned day.posts map yields the correct order (post with later updated_at first, otherwise ordered by occursAt).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/posts/src/views/ContentCalendar/utils/calendar.ts`:
- Around line 142-157: sortCalendarPosts currently calls posts.sort(...) which
mutates the input array; change it to return a new sorted array instead (e.g.,
use posts.toSorted(...) if available or return [...posts].sort(...)) so callers
aren't surprised by in-place mutation. Update the implementation inside the
sortCalendarPosts function to operate on a non-mutating copy and preserve the
existing comparison logic for 'updated_at desc', 'published_at asc', and the
default branch.
In `@apps/posts/test/unit/utils/content-calendar.test.ts`:
- Around line 56-70: Add a unit test that verifies ordering when order is
'updated_at desc' by calling buildCalendarGrid with order: 'updated_at desc' and
posts where one post has updated_at and another lacks updated_at so the logic
falls back to occursAt; use createPost to craft posts (e.g., one with updated_at
later and one without updated_at but different occursAt) locate the target day
via grid.find(day => day.dateKey === 'YYYY-MM-DD') and assert the returned
day.posts map yields the correct order (post with later updated_at first,
otherwise ordered by occursAt).
Introducing a full Content Calendar experience for Posts, moving beyond scheduled-only items.

What’s included
Please take a minute to explain the change you're making:
Please check your PR against these items:
Note
Medium Risk
Adds a large new UI surface that builds NQL filters and date-grouping logic, which could impact post querying/performance and edge-case correctness across timezones. Changes are mostly additive with unit coverage, but touch navigation and shared post typing.
Overview
Adds a new Posts Content Calendar view (
/posts/calendar) that fetches posts and renders a month grid with status-based styling, month navigation, and deep-linked filtering/sorting via URL search params (type/visibility/author/tag/order) with role-aware defaults.Updates the admin sidebar Posts submenu to include a
Calendarentry with correct active-state handling, extends the sharedPostAPI type to includecreated_at/updated_atfor calendar placement logic, and adds calendar utility + unit tests covering timezone date keys, ordering, and date-field fallbacks. Also adjusts the dev MySQL container healthcheck to usemysqladmin pinginstead of a query.Written by Cursor Bugbot for commit 8e4c881. This will update automatically on new commits. Configure here.