Conversation
Adds a multi-calendar export option to the export dialog so users can combine shifts from multiple calendars into a single export file. Adds a toggle and selectable calendar list with animations, preselects the current calendar, validates selection, and switches the request flow to POST for multi-calendar exports. Wires available calendars through the settings/dialog flow and includes new i18n strings for the UI. Also updates the lockfile metadata.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthroughConverted single-calendar export flows into multi-calendar exports: replaced GET with POST for ICS/PDF endpoints, added rate limiting for ICS, extended backend queries and export generation to handle multiple calendars, added multi-calendar selection UI, and introduced localization strings for the feature. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Export Dialog
participant API as /api/export/[format]
participant RateLimit as Rate Limiter
participant DB as Database
participant FileGen as Export Generator
User->>UI: Select calendars & click Export
UI->>UI: Validate selection (>=1)
UI->>API: POST /api/export/{format} { calendarIds: [...] }
API->>RateLimit: check("export-ics") / check("export-pdf")
alt rate limit exceeded
RateLimit-->>API: 429
API-->>UI: 429 response
UI-->>User: show rate-limit error
else allowed
RateLimit-->>API: ok
API->>DB: fetch calendars by ids
DB-->>API: calendars
API->>DB: fetch shifts for accessible calendars
DB-->>API: shifts
API->>FileGen: generate ICS/PDF (multi-calendar metadata)
FileGen-->>API: file buffer
API-->>UI: file response (download)
UI-->>User: save file
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (3)components/**/*.{tsx,ts}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
components/**/*dialog*.{tsx,ts}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
🧠 Learnings (2)📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
🔇 Additional comments (7)
✏️ Tip: You can disable this entire section by setting 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.
Pull request overview
This PR adds multi-calendar export functionality allowing users to combine shifts from multiple calendars into a single export file (ICS or PDF). The implementation includes a new toggle UI in the export dialog with animated calendar selection, proper permission checking, and rate limiting.
Changes:
- Export dialog enhanced with multi-calendar toggle and selectable calendar list
- API routes converted from GET to POST to support array of calendar IDs in request body
- Rate limiting added for ICS exports to match PDF export protection
- Internationalization strings added for all three supported locales (en, de, it)
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| messages/en.json | Added English translations for multi-calendar export UI |
| messages/de.json | Added German translations for multi-calendar export UI |
| messages/it.json | Added Italian translations for multi-calendar export UI |
| lib/rate-limiter.ts | Added rate limiting configuration for ICS exports (20 requests per 10 minutes) |
| components/export-dialog.tsx | Enhanced dialog with multi-calendar toggle, calendar selection list, and POST request support |
| components/dialog-manager.tsx | Threaded availableCalendars prop to settings sheet |
| components/calendar-settings-sheet.tsx | Accepted and passed availableCalendars prop to export dialog |
| app/api/export/pdf/route.ts | Converted from GET to POST, added multi-calendar support with permission filtering |
| app/api/export/ics/route.ts | Converted from GET to POST, added multi-calendar support with rate limiting |
| .env.example | Documented new ICS export rate limit environment variables |
Comments suppressed due to low confidence (2)
app/api/export/pdf/route.ts:51
- The accessibleCalendars array should be typed explicitly. Declare it as
const accessibleCalendars: typeof requestedCalendars = []to maintain proper TypeScript typing and avoid implicit any[] type.
app/api/export/ics/route.ts:47 - The accessibleCalendars array should be typed explicitly. Declare it as
const accessibleCalendars: typeof requestedCalendars = []to maintain proper TypeScript typing and avoid implicit any[] type.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/export-dialog.tsx (1)
167-177: Fallback filename doesn't reflect multi-calendar export.When
Content-Dispositionheader is missing, the fallback filename usescalendarName(single calendar name) even in multi-calendar mode. This is a minor inconsistency since the server should always provide the header, but consider updating the fallback for completeness.🔧 Suggested fix
// Get filename from Content-Disposition header or create one const contentDisposition = response.headers.get("Content-Disposition"); - let filename = `${calendarName - .replace(/[^a-z0-9]/gi, "_") - .toLowerCase()}_export.${exportFormat}`; + let filename = multiCalendar + ? `multi_calendar_export.${exportFormat}` + : `${calendarName + .replace(/[^a-z0-9]/gi, "_") + .toLowerCase()}_export.${exportFormat}`;
🤖 Fix all issues with AI agents
In `@app/api/export/pdf/route.ts`:
- Around line 119-129: The hexToRgb helper currently only accepts 6‑digit hex
and returns black for shorthand like "#FFF"; update hexToRgb to normalize
shorthand by expanding 3‑digit hex to 6‑digit (or accept both via regex) before
parsing so "#FFF" becomes "#FFFFFF", and ensure it still falls back safely for
invalid input; also add validation in the ColorPicker input handler (the custom
color text field) to reject or normalize non‑hex values before they call
hexToRgb or are persisted.
🧹 Nitpick comments (4)
lib/rate-limiter.ts (1)
442-496: Pre-existing type inconsistency ingetRateLimitStatus.The
typeparameter union ingetRateLimitStatusis missing the admin-related types (admin-user-mutations,admin-password-reset,admin-bulk-operations,admin-calendar-mutations) that exist in therateLimitfunction. This is a pre-existing issue not introduced by this PR, but worth noting for future consistency.The
"export-ics"addition here is correct and matches the pattern.app/api/export/ics/route.ts (1)
40-47: Consider parallelizing permission checks.The sequential permission checks could be parallelized for better performance when exporting many calendars.
♻️ Optional optimization
- // Filter calendars by permission - only include calendars user can view - const accessibleCalendars = []; - for (const calendar of requestedCalendars) { - const hasAccess = await canViewCalendar(user?.id, calendar.id); - if (hasAccess) { - accessibleCalendars.push(calendar); - } - } + // Filter calendars by permission - only include calendars user can view + const permissionChecks = await Promise.all( + requestedCalendars.map(async (calendar) => ({ + calendar, + hasAccess: await canViewCalendar(user?.id, calendar.id), + })) + ); + const accessibleCalendars = permissionChecks + .filter(({ hasAccess }) => hasAccess) + .map(({ calendar }) => calendar);components/export-dialog.tsx (1)
261-271: RedundantonCheckedChangehandler on disabled checkbox.The
Checkboxhaspointer-events-nonewhich prevents user interaction, so theonCheckedChangehandler will never be called. The parentmotion.div'sonClickhandles all toggling. Consider removing the redundant handler for clarity.♻️ Simplify checkbox
<Checkbox checked={isSelected} - onCheckedChange={() => { - setSelectedCalendarIds((prev) => - prev.includes(calendar.id) - ? prev.filter((id) => id !== calendar.id) - : [...prev, calendar.id] - ); - }} className="pointer-events-none" />app/api/export/pdf/route.ts (1)
95-100: Redundant sort after DB query.The shifts are already sorted by
datein the database query (line 66), making this manual sort unnecessary.♻️ Remove redundant sort
- // Sort all shifts by date (already sorted from DB, but ensure it) - allShifts.sort((a, b) => { - const dateA = (a.date as Date).getTime(); - const dateB = (b.date as Date).getTime(); - return dateA - dateB; - });
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
.env.exampleapp/api/export/ics/route.tsapp/api/export/pdf/route.tscomponents/calendar-settings-sheet.tsxcomponents/dialog-manager.tsxcomponents/export-dialog.tsxlib/rate-limiter.tsmessages/de.jsonmessages/en.jsonmessages/it.json
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Store and display dates asYYYY-MM-DDstrings (local dates without timezone conversion). UseformatDateToLocal()fromlib/date-utils.tsbefore saving to database anddate-fnswith locale fromgetDateLocale()for display.
Use Drizzle-inferred types from schema directly (noanytypes). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. Noanytypes allowed. Import inferred types from Drizzle schema.
UseformatDateToLocal(date)utility fromlib/date-utils.tsto convert dates toYYYY-MM-DDformat before saving to database.
Files:
lib/rate-limiter.tscomponents/dialog-manager.tsxapp/api/export/pdf/route.tscomponents/export-dialog.tsxcomponents/calendar-settings-sheet.tsxapp/api/export/ics/route.ts
components/**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Components in
/componentsdirectory should be"use client"by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.
Files:
components/dialog-manager.tsxcomponents/export-dialog.tsxcomponents/calendar-settings-sheet.tsx
components/**/*dialog*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use the Dialog pattern (
components/ui/dialog.tsxfrom shadcn/ui) for list views, confirmations, and read-only displays.
Files:
components/dialog-manager.tsxcomponents/export-dialog.tsx
app/**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All components under
app/are Server Components by default unless marked with"use client". Use"use client"directive only for interactive UI components.
Files:
app/api/export/pdf/route.tsapp/api/export/ics/route.ts
app/api/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/api/**/*.ts: All API routes must includeGET/POST/PUT/PATCH/DELETEexports inapp/api/**directory and follow the pattern: check permissions → perform database operation → emit SSE event.
UsegetSessionUser(request.headers)fromlib/auth/sessions.tsto get current user in API routes. Use Better Auth'sauthClientmethods on the client side for session management.
After any database mutation (create/update/delete), emit an SSE event usingeventEmitter.emit('calendar-change', {...})with appropriate type and action to keep clients in sync.
For calendar permission hierarchy in SQL queries and permission checks, follow:owner(highest) >admin>write>read(lowest). UsegetUserAccessibleCalendars(userId)to get all accessible calendars.
UsegetUserCalendarPermission(userId, calendarId)fromlib/auth/permissions.tsto retrieve the exact permission level, which returns permission string ornullif user has no access.
Files:
app/api/export/pdf/route.tsapp/api/export/ics/route.ts
components/**/*sheet*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use the Sheet pattern (
components/ui/base-sheet.tsx) for all forms with side panels, including unsaved changes confirmation viauseDirtyStatehook and consistent header/footer styling with gradient backgrounds.
Files:
components/calendar-settings-sheet.tsx
🧠 Learnings (6)
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.
Applied to files:
components/dialog-manager.tsxcomponents/export-dialog.tsxcomponents/calendar-settings-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*dialog*.{tsx,ts} : Use the Dialog pattern (`components/ui/dialog.tsx` from shadcn/ui) for list views, confirmations, and read-only displays.
Applied to files:
components/dialog-manager.tsxcomponents/export-dialog.tsxcomponents/calendar-settings-sheet.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.
Applied to files:
app/api/export/pdf/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.
Applied to files:
app/api/export/pdf/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : All API routes must include `GET/POST/PUT/PATCH/DELETE` exports in `app/api/**` directory and follow the pattern: check permissions → perform database operation → emit SSE event.
Applied to files:
app/api/export/pdf/route.tsapp/api/export/ics/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*sheet*.{tsx,ts} : Use the Sheet pattern (`components/ui/base-sheet.tsx`) for all forms with side panels, including unsaved changes confirmation via `useDirtyState` hook and consistent header/footer styling with gradient backgrounds.
Applied to files:
components/calendar-settings-sheet.tsx
🧬 Code graph analysis (4)
app/api/export/pdf/route.ts (3)
app/api/export/ics/route.ts (1)
POST(11-192)lib/db/index.ts (1)
db(19-19)lib/db/schema.ts (3)
calendars(100-124)user(8-28)shifts(216-249)
components/export-dialog.tsx (4)
lib/types.ts (1)
CalendarWithCount(4-18)components/ui/dialog.tsx (4)
DialogContent(141-141)DialogHeader(144-144)DialogTitle(147-147)DialogDescription(142-142)components/ui/checkbox.tsx (1)
Checkbox(32-32)components/ui/label.tsx (1)
Label(24-24)
components/calendar-settings-sheet.tsx (1)
lib/types.ts (1)
CalendarWithCount(4-18)
app/api/export/ics/route.ts (4)
app/api/export/pdf/route.ts (1)
POST(10-315)lib/db/schema.ts (3)
user(8-28)calendars(100-124)shifts(216-249)lib/rate-limiter.ts (1)
rateLimit(314-436)lib/date-utils.ts (1)
getServerTimezone(159-179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: build-dev
🔇 Additional comments (16)
components/dialog-manager.tsx (1)
153-153: LGTM!The
availableCalendarsprop is correctly threaded fromprops.calendars(typed asCalendarWithCount[]) toCalendarSettingsSheet, enabling the multi-calendar export feature downstream.components/calendar-settings-sheet.tsx (2)
27-27: LGTM!The
CalendarWithCounttype is properly imported and used for the new optionalavailableCalendarsprop with a sensible default of[]. This maintains type safety throughout the component.Also applies to: 39-39, 57-57
318-324: LGTM!The
availableCalendarsprop is correctly forwarded toExportDialog, completing the data flow for multi-calendar export functionality.lib/rate-limiter.ts (2)
101-105: LGTM!The
exportIcsrate limit configuration follows the established pattern with sensible defaults (20 requests per 10 minutes). The higher limit compared to PDF export (10 requests) is appropriate since ICS generation is typically less resource-intensive.
376-378: LGTM!The switch case correctly maps
"export-ics"toconfig.exportIcs, consistent with the existing pattern for other rate limit types..env.example (1)
136-137: LGTM!The new environment variable examples are correctly documented with values matching the defaults in
rate-limiter.ts(20 requests, 600 seconds). The placement under "Export Features" maintains logical organization.messages/de.json (1)
432-438: LGTM!The German translations are grammatically correct and follow the established ICU MessageFormat pattern for pluralization. The new keys provide comprehensive coverage for the multi-calendar export UI including the toggle, calendar selection, count display, and validation message.
messages/en.json (1)
432-438: LGTM!The new localization keys for multi-calendar export are well-structured and follow the existing patterns. The ICU plural syntax in
selectedCountis correct for handling zero, singular, and plural cases.messages/it.json (1)
432-438: LGTM!Italian translations are consistent with the English keys and use correct plural forms for the language.
app/api/export/ics/route.ts (2)
91-177: LGTM!The ICS generation logic correctly handles:
- Multi-calendar event prefixing
- All-day events per RFC 5545 (DTEND exclusive)
- Timed events with proper UTC conversion
- Filename sanitization with reasonable truncation
15-17: Missingawaiton rate limit check.Based on the rate limiter signature in
lib/rate-limiter.ts, therateLimitfunction returnsNextResponse | null. If this function is asynchronous, the missingawaitwould cause the rate limit check to always pass (the Promise object is truthy but not a NextResponse).Verify whether
rateLimitis synchronous or asynchronous:#!/bin/bash # Check the rateLimit function signature ast-grep --pattern $'export async function rateLimit($$$) { $$$ }' # Also check for non-async version ast-grep --pattern $'export function rateLimit($$$) { $$$ }'components/export-dialog.tsx (2)
55-56: LGTM!State initialization and cleanup logic correctly:
- Pre-selects the current calendar when dialog opens
- Resets all state including multi-calendar selections when dialog closes
- Dependencies array includes
calendarIdfor proper re-initializationAlso applies to: 89-103
293-412: LGTM!The export format selection, PDF range options, and action buttons are well-implemented with proper loading state management.
app/api/export/pdf/route.ts (3)
10-30: LGTM on POST conversion and validation.The API correctly:
- Converts to POST method for body parsing
- Validates
calendarIdsarray- Returns appropriate 400 error for invalid input
The same
rateLimitawait question applies here as in the ICS route.
212-283: LGTM!PDF rendering correctly handles:
- Per-shift color indicators
- Multi-calendar calendar name display
- Dynamic layout positioning
- Month grouping and page breaks
288-300: LGTM!Filename generation is consistent with the ICS route pattern - sanitizing, truncating to 20 chars, limiting to 3 calendar names, and appending the current date.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Adds a multi-calendar export option to the export dialog so users can combine shifts from multiple calendars into a single export file.
Adds a toggle and selectable calendar list with animations, preselects the current calendar, validates selection, and switches the request flow to POST for multi-calendar exports. Wires available calendars through the settings/dialog flow and includes new i18n strings for the UI. Also updates the lockfile metadata.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Localization
✏️ Tip: You can customize this high-level summary in your review settings.