-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Export commissions as CSV #2755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a commissions CSV export feature: a new GET API route that validates export query and columns, a modal UI to select columns and include current filters, and popover wiring to open the modal and trigger the export download. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Commissions Page
participant Pop as Popover
participant Modal as ExportCommissionsModal
participant API as GET /api/commissions/export
participant DB as Prisma
User->>UI: Open commissions popover
User->>Pop: Click "Export as CSV"
Pop->>Modal: setShowExportCommissionsModal(true)
User->>Modal: Select columns / apply filters, Submit
Modal->>API: GET with workspaceId, columns, filters
API->>DB: Query commissions (invoiceId OR date window + filters)
DB-->>API: Result set (≤1000)
API->>API: Validate & map rows, build CSV
API-->>Modal: CSV response (blob)
Modal->>User: Trigger CSV download
sequenceDiagram
participant Client
participant Zod as commissionsExportQuerySchema
participant Prisma
participant Formatter as CSV Builder
participant FS as Response
Client->>Zod: Send searchParams (columns, filters)
Zod-->>Client: Validated params
Client->>Prisma: Build & run query
Prisma-->>Client: Rows
Client->>Formatter: Map rows, apply per-column schemas, serialize CSV
Formatter-->>FS: Return CSV stream
FS-->>Client: Respond with text/csv + Content-Disposition
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
43-45: Align title casing with surrounding UI (nit)Elsewhere (e.g., the modal header at Line 111 in export-commissions-modal.tsx), “Export commissions” uses sentence case. Consider matching here for consistency.
- Export Commissions + Export commissionsapps/web/app/(ee)/api/commissions/export/route.ts (5)
70-110: Prefer Date objects over ISO strings for temporal filtersPassing toISOString is unnecessary and can introduce subtle timezone edge cases. Prisma accepts Date objects directly.
- createdAt: { - gte: startDate.toISOString(), - lte: endDate.toISOString(), - }, + createdAt: { + gte: startDate, + lte: endDate, + },
27-40: Simplify and harden type transforms for CSV shaping
- number: using
value || 0conflates 0 and NaN/null; prefer nullish coalescing.- date: since inputs are Date, coercion isn’t needed;
?.is redundant.- string: fine, but you can avoid extra default by using nullish coalescing.
const COLUMN_TYPE_SCHEMAS = { - number: z.coerce - .number() - .nullable() - .default(0) - .transform((value) => value || 0), - date: z.date().transform((date) => date?.toISOString() || ""), - string: z - .string() - .nullable() - .default("") - .transform((value) => value || ""), + number: z.number().nullable().transform((value) => value ?? 0), + date: z.date().transform((date) => date.toISOString()), + string: z.string().nullable().transform((value) => value ?? ""), };
141-144: Use human-friendly column labels and preserve header order even for empty datasetsCurrently headers will be column IDs. If you want labels in the CSV, map keys to labels in the selected order before conversion. This also ensures headers are present even when no rows match.
- const data = z.array(z.object(columnSchemas)).parse(formattedCommissions); - const csvData = convertToCSV(data); + const data = z.array(z.object(columnSchemas)).parse(formattedCommissions); + const labeledData = data.map((row) => { + const out: Record<string, unknown> = {}; + for (const id of columns) { + const label = COLUMN_LOOKUP.get(id)?.label ?? id; + // @ts-expect-error index access at runtime + out[label] = row[id]; + } + return out; + }); + const csvData = convertToCSV(labeledData);If convertToCSV already supports a header mapping argument, prefer using that instead of re-keying objects.
43-43: Confirm permissions/plan gating for export endpointIf CSV export should be restricted (e.g., to specific roles or plans), pass options to withWorkspace to enforce it. Otherwise, this endpoint inherits only the base workspace checks.
Example (adjust for your permission constants):
export const GET = withWorkspace( async ({ searchParams, workspace }) => { /* ... */ }, { requiredPermissions: ["commissions:read"] /*, requiredPlan: ["pro","enterprise"]*/ }, );
106-109: Hard cap of 1000 rows—verify product intentThe API silently truncates to 1000 rows. If that’s intentional, consider reflecting it in the UI copy or providing pagination/chunked exporting later.
apps/web/lib/zod/schemas/commissions.ts (1)
255-279: Trim and dedupe incoming columns for resilienceUsers or clients could send space-delimited/duplicate IDs. Normalize the array before validation.
export const commissionsExportQuerySchema = getCommissionsQuerySchema .omit({ page: true, pageSize: true }) .merge( z.object({ columns: z .string() - .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(",")) - .transform((v) => v.split(",")) + .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(",")) + .transform((v) => + Array.from( + new Set( + v + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + ), + ), + ) .refine( (columns): columns is CommissionExportColumnId[] => { const validColumnIds = COMMISSION_EXPORT_COLUMNS.map( (col) => col.id, );apps/web/ui/modals/export-commissions-modal.tsx (4)
55-59: Provide user feedback when context is missingIf workspace or program isn’t resolved, return with a toast so users know why nothing happened.
const onSubmit = handleSubmit(async (data) => { if (!workspaceId || !program?.id) { - return; + toast.error("Unable to export: missing workspace or program context."); + return; }
74-80: Avoid unnecessary headers on GETContent-Type isn’t needed for GET and may trigger CORS preflight in some setups.
- const response = await fetch(`/api/commissions/export${searchParams}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const response = await fetch(`/api/commissions/export${searchParams}`, { + method: "GET", + });
81-85: Surface clean error messages in toastsPassing an Error object to sonner will render “[object Object]”. Use the message string.
- if (!response.ok) { - const { error } = await response.json(); - throw new Error(error.message); - } + if (!response.ok) { + const { error } = await response.json().catch(() => ({})); + throw new Error(error?.message || "Failed to export commissions."); + } @@ - } catch (error) { - toast.error(error); + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error));Also applies to: 96-101
86-93: Use server-suggested filename and make it filesystem-safe; revoke object URL
- Parse Content-Disposition for a server-suggested filename (keeps API/UI in sync).
- Sanitize timestamp fallback to avoid colons.
- Revoke the URL to free memory.
- const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - - a.href = url; - a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`; - a.click(); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + const disposition = response.headers.get("content-disposition"); + const suggestedFilename = disposition?.match(/filename="(.+?)"/)?.[1]; + const timestamp = new Date().toISOString().replace(/:/g, "-"); + const filename = + suggestedFilename || `Dub Commissions Export - ${timestamp}.csv`; + a.href = url; + a.download = filename; + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 0);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/web/app/(ee)/api/commissions/export/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx(3 hunks)apps/web/lib/zod/schemas/commissions.ts(1 hunks)apps/web/ui/modals/export-commissions-modal.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
apps/web/lib/zod/schemas/commissions.ts (3)
COMMISSION_EXPORT_COLUMNS(213-245)commissionsExportQuerySchema(255-279)getCommissionsQuerySchema(44-98)apps/web/lib/auth/workspace.ts (1)
withWorkspace(41-435)
apps/web/ui/modals/export-commissions-modal.tsx (4)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/zod/schemas/commissions.ts (2)
DEFAULT_COMMISSION_EXPORT_COLUMNS(250-253)COMMISSION_EXPORT_COLUMNS(213-245)apps/web/lib/openapi/index.ts (1)
document(23-83)packages/ui/src/tooltip.tsx (1)
InfoTooltip(193-199)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
apps/web/ui/modals/export-commissions-modal.tsx (1)
useExportCommissionsModal(177-197)
🔇 Additional comments (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (2)
15-17: Clean integration of the export modal hookThe modal is correctly instantiated once and mounted in-tree; the setter wiring is sound.
Also applies to: 21-21
42-58: Popover action correctly opens the export modalClosing the popover before opening the modal prevents overlay conflicts. The UI pattern matches the existing “Create clawback” action.
apps/web/lib/zod/schemas/commissions.ts (1)
213-245: Well-structured, typed column catalogClear ids, labels, and types with defaults will make the UI and server code robust and consistent.
apps/web/ui/modals/export-commissions-modal.tsx (3)
118-154: Columns controller and checkbox grid look goodState updates are minimal and correctly derived; IDs are properly associated with labels for accessibility.
171-171: Button loading state integration is correctThe submit button correctly reflects isSubmitting state.
177-196: Stable modal hook APIThe memoized component wrapper and setter export make consumption simple and avoid unnecessary remounts.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
92-101: Group filter is now applied in the export query (resolved)The conditional partner group filter is included via partner.programs.some({ programId, groupId }). This addresses the earlier omission.
156-163: Filesystem-safe filename handled (resolved)Replacing colons in the ISO timestamp fixes Windows filename issues. Thanks for addressing this.
🧹 Nitpick comments (9)
apps/web/lib/zod/schemas/commissions.ts (2)
259-277: Harden columns parsing: trim, dedupe, and return precise validation errorsCurrent parsing accepts whitespace and duplicates and returns a generic error. Tighten it to:
- trim each id
- drop empties
- dedupe
- surface invalid ids in the error message
Apply this diff:
- columns: z - .string() - .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(",")) - .transform((v) => v.split(",")) - .refine( - (columns): columns is CommissionExportColumnId[] => { - const validColumnIds = COMMISSION_EXPORT_COLUMNS.map( - (col) => col.id, - ); - - return columns.every((column): column is CommissionExportColumnId => - validColumnIds.includes(column as CommissionExportColumnId), - ); - }, - { - message: - "Invalid column IDs provided. Please check the available columns.", - }, - ), + columns: z + .string() + .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(",")) + .transform((v) => + Array.from( + new Set( + v + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + ), + ), + ) + .superRefine((columns, ctx) => { + const validIds = new Set( + COMMISSION_EXPORT_COLUMNS.map((col) => col.id), + ); + const invalid = columns.filter((c) => !validIds.has(c)); + if (invalid.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid column IDs: ${invalid.join(", ")}`, + }); + } + }),
247-253: Export the column id type and type the defaultsExporting the id type improves type-safety in consumers (UI, route) and typing the defaults prevents accidental drift.
-type CommissionExportColumnId = +export type CommissionExportColumnId = (typeof COMMISSION_EXPORT_COLUMNS)[number]["id"]; -export const DEFAULT_COMMISSION_EXPORT_COLUMNS = +export const DEFAULT_COMMISSION_EXPORT_COLUMNS: CommissionExportColumnId[] = COMMISSION_EXPORT_COLUMNS.filter((column) => column.default).map( (column) => column.id, );apps/web/app/(ee)/api/commissions/export/route.ts (2)
89-91: Prefer passing Date objects to Prisma instead of ISO stringsPrisma accepts Date objects directly. Avoiding toISOString keeps types consistent and eliminates needless string conversion.
- createdAt: { - gte: startDate.toISOString(), - lte: endDate.toISOString(), - }, + createdAt: { + gte: startDate, + lte: endDate, + },
13-25: Tighten typing for column types to prevent mismatchesMap value currently uses
type: string, which weakens type-safety when indexingCOLUMN_TYPE_SCHEMAS. Constrain it to the known keys.-const COLUMN_LOOKUP: Map< - string, - { label: string; type: string; order: number } -> = new Map( +type ColumnType = keyof typeof COLUMN_TYPE_SCHEMAS; +const COLUMN_LOOKUP: Map< + string, + { label: string; type: ColumnType; order: number } +> = new Map(apps/web/ui/modals/export-commissions-modal.tsx (5)
91-93: Sanitize the download filename and revoke the blob URLColons break Windows filenames and the blob URL should be revoked to avoid leaks. The server already sets a safe filename; keep it consistent client-side.
- a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`; - a.click(); + a.download = `Dub Commissions Export - ${new Date() + .toISOString() + .replace(/:/g, "-")}.csv`; + a.click(); + a.remove(); + // Revoke on next tick to avoid interfering with download + setTimeout(() => URL.revokeObjectURL(url), 0);
76-79: Drop the Content-Type header on GETUnnecessary for a GET and can be omitted.
- const response = await fetch(`/api/commissions/export${searchParams}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const response = await fetch(`/api/commissions/export${searchParams}`, { + method: "GET", + });
63-69: Guard against exporting with zero columns (better UX)If no columns are selected, the server will 400. Short-circuit with a clear message.
const params = { workspaceId, ...(data.columns.length ? { columns: data.columns.join(",") } : undefined), }; + if (data.columns.length === 0) { + throw new Error("Select at least one column to export."); + }
96-100: Ensure toast displays a useful message for unknown errors
toast.error(error)may render[object Object]. Use a string message fallback.- } catch (error) { - toast.error(error); + } catch (error) { + const message = + error instanceof Error + ? error.message + : "Failed to export commissions. Please try again."; + toast.error(message); } finally { toast.dismiss(lid); }
55-58: Handle missing context explicitlyIf context is missing, surface a user-facing error instead of silently returning.
- if (!workspaceId || !program?.id) { - return; - } + if (!workspaceId || !program?.id) { + toast.error("Missing workspace or program context."); + return; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/web/app/(ee)/api/commissions/export/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx(3 hunks)apps/web/lib/zod/schemas/commissions.ts(1 hunks)apps/web/ui/modals/export-commissions-modal.tsx(1 hunks)
🔇 Additional comments (3)
apps/web/lib/zod/schemas/commissions.ts (1)
213-245: Column config looks comprehensive and consistentGood coverage of core commission, partner, and customer fields, with sensible defaults and types.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
15-22: Clean integration of Export modal into the popoverHook wiring and rendering look good; UX flows correctly from popover → modal.
apps/web/app/(ee)/api/commissions/export/route.ts (1)
117-121: Verify the 1,000-row cap in commissions exportThe commissions export endpoint (apps/web/app/(ee)/api/commissions/export/route.ts:117) is hard-capped at
take: 1000. Please confirm this limit matches product expectations. If users require full exports, consider one or more of the following:
- Raising the cap (e.g., to 10 000)
- Implementing chunked or streaming exports
- Exposing pagination (page/token) to fetch additional rows
Note that other list endpoints in
apps/web/appdefault to much smaller limits (50–100 rows), so ensure any new cap remains consistent across APIs.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
92-101: Resolved: groupId filter now applied in the queryThe conditional partner.programs some { programId, groupId } fixes the earlier omission.
167-175: Filename now sanitized for Windows compatibilityGood fix replacing colons in the ISO timestamp before using it in Content-Disposition.
🧹 Nitpick comments (6)
apps/web/lib/zod/schemas/commissions.ts (1)
267-291: Deduplicate and sanitize column IDs at parse-timeAvoid duplicates and empty IDs from malformed queries (e.g., trailing commas) by filtering empties and deduping after split.
Apply this diff:
export const commissionsExportQuerySchema = getCommissionsQuerySchema .omit({ page: true, pageSize: true }) .merge( z.object({ columns: z .string() .default(DEFAULT_COMMISSION_EXPORT_COLUMNS.join(",")) - .transform((v) => v.split(",")) + .transform((v) => v.split(",").filter(Boolean)) + .transform((arr) => Array.from(new Set(arr))) .refine( (columns): columns is CommissionExportColumnId[] => { const validColumnIds = COMMISSION_EXPORT_COLUMNS.map( (col) => col.id, ); return columns.every((column): column is CommissionExportColumnId => validColumnIds.includes(column as CommissionExportColumnId), ); }, { message: "Invalid column IDs provided. Please check the available columns.", }, ), }), );apps/web/app/(ee)/api/commissions/export/route.ts (2)
88-91: Prefer Date objects over ISO strings in Prisma filtersPrisma accepts Date objects directly; passing Dates avoids any potential timezone/string parsing edge cases.
Apply this diff:
createdAt: { - gte: startDate.toISOString(), - lte: endDate.toISOString(), + gte: startDate, + lte: endDate, },
144-148: Guard against duplicate columns before sortingIf duplicates sneak in, the CSV would repeat columns. Dedup here as a final safeguard.
Apply this diff:
- columns = columns.sort( + columns = [...new Set(columns)].sort( (a, b) => (COLUMN_LOOKUP.get(a)?.order || 999) - (COLUMN_LOOKUP.get(b)?.order || 999), );apps/web/ui/modals/export-commissions-modal.tsx (3)
74-79: Remove unnecessary Content-Type header on GETNo body is sent; some intermediaries treat GET+Content-Type oddly.
Apply this diff:
- const response = await fetch(`/api/commissions/export${searchParams}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const response = await fetch(`/api/commissions/export${searchParams}`);
97-100: Surface the actual error message in toastPassing the Error object directly can render as [object Object].
Apply this diff:
- } catch (error) { - toast.error(error); + } catch (error) { + toast.error(error instanceof Error ? error.message : String(error));
123-155: Optional: prevent submitting with zero columns selectedIf a user unchecks everything, the backend falls back to defaults, which might be surprising. Consider disabling submit or showing a validation error when no columns are selected.
I can add a small client-side guard to disable the submit button when columns.length === 0, if desired.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/web/app/(ee)/api/commissions/export/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx(3 hunks)apps/web/lib/zod/schemas/commissions.ts(1 hunks)apps/web/ui/modals/export-commissions-modal.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
apps/web/ui/modals/export-commissions-modal.tsx (1)
useExportCommissionsModal(178-198)
apps/web/app/(ee)/api/commissions/export/route.ts (2)
apps/web/lib/zod/schemas/commissions.ts (3)
COMMISSION_EXPORT_COLUMNS(213-257)commissionsExportQuerySchema(267-291)getCommissionsQuerySchema(44-98)apps/web/lib/auth/workspace.ts (1)
withWorkspace(41-435)
apps/web/ui/modals/export-commissions-modal.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/zod/schemas/commissions.ts (2)
DEFAULT_COMMISSION_EXPORT_COLUMNS(262-265)COMMISSION_EXPORT_COLUMNS(213-257)packages/ui/src/tooltip.tsx (1)
InfoTooltip(193-199)
🔇 Additional comments (5)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-popover-buttons.tsx (1)
15-22: Export modal wiring looks goodPopover closes before opening the export modal, modal is mounted at the root of the fragment, and the hook usage is clean.
Also applies to: 42-58
apps/web/lib/zod/schemas/commissions.ts (1)
213-257: Comprehensive, clear column inventoryGood coverage and clear labels/types. This aligns well with the server route’s formatting needs.
apps/web/app/(ee)/api/commissions/export/route.ts (2)
150-166: Confirm header labels vs. field keys in CSV outputIf convertToCSV uses object keys for headers, the CSV will show keys like partnerName rather than human-friendly labels like "Partner name". If convertToCSV supports a header map, consider passing labels to match COMMISSION_EXPORT_COLUMNS. Otherwise, prepend a header row using the sorted labels.
Would you like me to adapt convertToCSV usage to include a header mapping derived from columns -> COLUMN_LOOKUP labels?
43-46: Verify route permission gating matches commissions list permissionsThe export endpoint should be at least as restricted as viewing commissions. Ensure withWorkspace enforces appropriate permissions/plan. If your pattern is withWorkspace({ requiredPermissions: [...] })(handler) or withWorkspace(handler, { ... }), wire this route accordingly.
I can submit a follow-up patch once you confirm the exact permission string used for reading commissions in this codebase.
apps/web/ui/modals/export-commissions-modal.tsx (1)
178-198: Hook/component pattern reads cleanlyNice ergonomic API: memoized component + setter. Minimizes prop drilling and keeps the entry point simple.
| a.href = url; | ||
| a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`; | ||
| a.click(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use server-provided filename (or sanitize) and revoke object URL
Avoid illegal colons in filenames on Windows and potential memory leaks by revoking the object URL.
Apply this diff:
- a.href = url;
- a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`;
- a.click();
+ const disposition = response.headers.get("Content-Disposition");
+ let filename = `Dub Commissions Export - ${new Date()
+ .toISOString()
+ .replace(/:/g, "-")}.csv`;
+ if (disposition) {
+ const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(disposition);
+ if (match?.[1]) {
+ filename = decodeURIComponent(match[1].replace(/^"|"$/g, ""));
+ }
+ }
+
+ a.href = url;
+ a.download = filename;
+ a.click();
+ setTimeout(() => URL.revokeObjectURL(url), 0);📝 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.
| a.href = url; | |
| a.download = `Dub Commissions Export - ${new Date().toISOString()}.csv`; | |
| a.click(); | |
| const disposition = response.headers.get("Content-Disposition"); | |
| let filename = `Dub Commissions Export - ${new Date() | |
| .toISOString() | |
| .replace(/:/g, "-")}.csv`; | |
| if (disposition) { | |
| const match = /filename\*?=(?:UTF-8''|")?([^;\r\n"]+)/i.exec(disposition); | |
| if (match?.[1]) { | |
| filename = decodeURIComponent(match[1].replace(/^"|"$/g, "")); | |
| } | |
| } | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| setTimeout(() => URL.revokeObjectURL(url), 0); |
🤖 Prompt for AI Agents
In apps/web/ui/modals/export-commissions-modal.tsx around lines 90 to 93, the
download filename uses an ISO timestamp with colons (invalid on Windows) and the
created object URL is never revoked causing a potential memory leak; fix by
deriving the filename from the server Content-Disposition header when available
(fall back to a sanitized default), replace or remove illegal characters (e.g.,
replace ":" with "-" in timestamps), set a.download to that sanitized filename,
call a.click(), then revoke the object URL via URL.revokeObjectURL(url) and
clean up the temporary anchor element.
Summary by CodeRabbit