feat: 頒布履歴ページにCSVエクスポート機能を追加 - #1
Conversation
売上明細をCSVファイルとしてダウンロードできる機能を追加。 日別表示時は日付ごと、カタログ別表示時はカタログごとにエクスポート可能。 CSVカラム: 日時, カタログ名, 商品名, 単価, 数量, 小計 https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
Heroicons の arrow-down-tray (mini) アイコンを ExportButton に追加し、 アイコン + "CSV" テキストの構成にした。 https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
📝 WalkthroughWalkthroughCSV export is added: a new utility module generates and downloads CSV from sales data (escaping fields, formatting datetimes, sanitizing filenames). The SaleList UI includes Export buttons for daily and catalog views that call these utilities and trigger browser downloads. Changes
Sequence DiagramsequenceDiagram
actor User
participant SaleList as "SaleList Component"
participant CSVUtil as "CSV Utilities"
participant Browser as "Browser Download"
User->>SaleList: Click Export button (daily or catalog)
activate SaleList
SaleList->>CSVUtil: generateSalesCsv(sales, catalogNameResolver)
activate CSVUtil
CSVUtil->>CSVUtil: Flatten sales → items, calc subtotals
CSVUtil->>CSVUtil: formatDateTime / escapeCsvField / sanitizeFilename
CSVUtil-->>SaleList: Return CSV string
deactivate CSVUtil
SaleList->>CSVUtil: downloadCsv(csvString, filename)
activate CSVUtil
CSVUtil->>Browser: Create Blob & object URL
CSVUtil->>Browser: Create temporary link, trigger click
CSVUtil->>Browser: Revoke object URL, remove link
deactivate CSVUtil
deactivate SaleList
Browser-->>User: CSV file downloaded
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/SaleList.tsx`:
- Around line 223-234: The download filename uses raw catalogName which may
contain characters invalid in filenames; update handleExportCatalog to sanitize
catalogName before using it: remove or replace characters like \/:*?"<>| and
control characters (and optionally trim/limit length), then use the sanitized
value in the call to downloadCsv (still format exportTime via
formatDateTimeForFilename). Ensure the change targets the handleExportCatalog
function and the variable passed into downloadCsv so filenames are safe across
environments.
In `@src/utils/csvExport.ts`:
- Around line 57-59: The CSV download currently creates a Blob from csvContent
in downloadCsv without a UTF-8 BOM, which causes Japanese text to garble in
Excel; modify downloadCsv to prepend the UTF-8 BOM character (U+FEFF) to
csvContent before creating the Blob (so the Blob is constructed from "\uFEFF" +
csvContent) and keep the MIME type as 'text/csv;charset=utf-8' and the rest of
the flow (URL.createObjectURL, link download) unchanged.
- Around line 5-10: escapeCsvField currently only handles commas, quotes and
newlines but doesn't protect against spreadsheet formula injection; update the
escapeCsvField function to detect if the field begins with any of the dangerous
characters (=, +, -, @, tab, carriage return) and if so prefix the field with a
single quote (') before performing the normal quote-escaping and
surrounding-quote logic; ensure you still replace internal " with "" and apply
the surrounding quotes when the field contains comma, quote, newline or the
injected leading character so the returned string both prevents formula
execution and remains valid CSV.
🪄 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: d0287154-4e5f-4eeb-848e-f6deda1a66e3
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
src/pages/SaleList.tsxsrc/utils/csvExport.test.tssrc/utils/csvExport.ts
| export const escapeCsvField = (field: string): string => { | ||
| if (field.includes(',') || field.includes('"') || field.includes('\n')) { | ||
| return `"${field.replace(/"/g, '""')}"`; | ||
| } | ||
| return field; | ||
| }; |
There was a problem hiding this comment.
Harden CSV escaping against spreadsheet formula injection.
Fields beginning with =, +, -, @, tab, or carriage return can be interpreted as formulas by spreadsheet tools.
🔒 Proposed fix
export const escapeCsvField = (field: string): string => {
- if (field.includes(',') || field.includes('"') || field.includes('\n')) {
- return `"${field.replace(/"/g, '""')}"`;
+ const neutralized = /^[=+\-@\t\r]/.test(field) ? `'${field}` : field;
+ if (
+ neutralized.includes(',') ||
+ neutralized.includes('"') ||
+ neutralized.includes('\n') ||
+ neutralized.includes('\r')
+ ) {
+ return `"${neutralized.replace(/"/g, '""')}"`;
}
- return field;
+ return neutralized;
};📝 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.
| export const escapeCsvField = (field: string): string => { | |
| if (field.includes(',') || field.includes('"') || field.includes('\n')) { | |
| return `"${field.replace(/"/g, '""')}"`; | |
| } | |
| return field; | |
| }; | |
| export const escapeCsvField = (field: string): string => { | |
| const neutralized = /^[=+\-@\t\r]/.test(field) ? `'${field}` : field; | |
| if ( | |
| neutralized.includes(',') || | |
| neutralized.includes('"') || | |
| neutralized.includes('\n') || | |
| neutralized.includes('\r') | |
| ) { | |
| return `"${neutralized.replace(/"/g, '""')}"`; | |
| } | |
| return neutralized; | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/csvExport.ts` around lines 5 - 10, escapeCsvField currently only
handles commas, quotes and newlines but doesn't protect against spreadsheet
formula injection; update the escapeCsvField function to detect if the field
begins with any of the dangerous characters (=, +, -, @, tab, carriage return)
and if so prefix the field with a single quote (') before performing the normal
quote-escaping and surrounding-quote logic; ensure you still replace internal "
with "" and apply the surrounding quotes when the field contains comma, quote,
newline or the injected leading character so the returned string both prevents
formula execution and remains valid CSV.
| export const downloadCsv = (csvContent: string, filename: string): void => { | ||
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); | ||
| const url = URL.createObjectURL(blob); |
There was a problem hiding this comment.
Prepend UTF-8 BOM for Excel compatibility with Japanese CSV.
Without BOM, Japanese headers/values are frequently garbled in Windows Excel.
🧾 Proposed fix
export const downloadCsv = (csvContent: string, filename: string): void => {
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
+ const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);📝 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.
| export const downloadCsv = (csvContent: string, filename: string): void => { | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| export const downloadCsv = (csvContent: string, filename: string): void => { | |
| const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/csvExport.ts` around lines 57 - 59, The CSV download currently
creates a Blob from csvContent in downloadCsv without a UTF-8 BOM, which causes
Japanese text to garble in Excel; modify downloadCsv to prepend the UTF-8 BOM
character (U+FEFF) to csvContent before creating the Blob (so the Blob is
constructed from "\uFEFF" + csvContent) and keep the MIME type as
'text/csv;charset=utf-8' and the rest of the flow (URL.createObjectURL, link
download) unchanged.
PRレビューの指摘に対応: - sanitizeFilename: カタログ名に含まれるファイル名不正文字を_に置換 - escapeCsvField: =,+,-,@,\t,\r で始まるフィールドにシングルクォートを プレフィックスしてスプレッドシートの数式インジェクションを防止 - 対応するユニットテストを追加 https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/utils/csvExport.ts (2)
7-14:⚠️ Potential issue | 🟠 MajorQuote
\r-containing fields to avoid broken CSV rows.Line 12 does not treat carriage return (
\r) as a quoting trigger, so fields with\rcan break record boundaries in CSV parsers.🔧 Proposed fix
export const escapeCsvField = (field: string): string => { - let sanitized = field; - if (sanitized.length > 0 && FORMULA_PREFIXES.includes(sanitized[0])) { + let sanitized = field; + const needsFormulaNeutralization = + sanitized.length > 0 && FORMULA_PREFIXES.includes(sanitized[0]); + if (needsFormulaNeutralization) { sanitized = `'${sanitized}`; } - if (sanitized.includes(',') || sanitized.includes('"') || sanitized.includes('\n')) { + if ( + needsFormulaNeutralization || + sanitized.includes(',') || + sanitized.includes('"') || + sanitized.includes('\n') || + sanitized.includes('\r') + ) { return `"${sanitized.replace(/"/g, '""')}"`; } return sanitized; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/csvExport.ts` around lines 7 - 14, The escapeCsvField function fails to treat carriage returns as a quoting trigger, which can break CSV rows; update the quoting condition in escapeCsvField (and any related checks around FORMULA_PREFIXES) to include '\r' alongside '\n', ',' and '"' so fields containing '\r' are wrapped and double-quoted via the existing replace logic; ensure the function still returns the sanitized value when no quoting is needed.
68-70:⚠️ Potential issue | 🟠 MajorPrepend UTF-8 BOM for Japanese CSV readability in Excel.
Line 69 builds the Blob without BOM, which often causes mojibake in Windows Excel for Japanese text.
🔧 Proposed fix
export const downloadCsv = (csvContent: string, filename: string): void => { - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); + const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/csvExport.ts` around lines 68 - 70, The CSV Blob is created without a UTF-8 BOM which causes mojibake in Excel for Japanese text; in downloadCsv, prepend the UTF-8 BOM (e.g. '\uFEFF' or the byte sequence 0xEF,0xBB,0xBF) to the csvContent before constructing the Blob (the code creating `const blob = new Blob([csvContent], ...)`), so replace that creation to include the BOM and then continue using URL.createObjectURL(blob) and the existing filename/path logic.
🧹 Nitpick comments (1)
src/utils/csvExport.ts (1)
70-77: Harden cleanup withtry/finallyaround temporary DOM/URL resources.If any step after URL creation throws, cleanup at Lines 76-77 may be skipped.
♻️ Proposed refactor
export const downloadCsv = (csvContent: string, filename: string): void => { const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + try { + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + } finally { + if (a.parentNode) document.body.removeChild(a); + URL.revokeObjectURL(url); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/csvExport.ts` around lines 70 - 77, Wrap the DOM/URL cleanup in a try/finally: after creating url via URL.createObjectURL(blob) and the anchor element a (document.createElement('a')), call document.body.appendChild(a) and perform a.click() inside a try block, and in the finally ensure document.body.removeChild(a) (only if appended) and URL.revokeObjectURL(url) are always executed; this guarantees the temporary anchor and object URL (url, a, URL.createObjectURL, URL.revokeObjectURL, a.click(), document.body.appendChild/removeChild) are cleaned up even if a.click() or other steps throw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/utils/csvExport.ts`:
- Around line 7-14: The escapeCsvField function fails to treat carriage returns
as a quoting trigger, which can break CSV rows; update the quoting condition in
escapeCsvField (and any related checks around FORMULA_PREFIXES) to include '\r'
alongside '\n', ',' and '"' so fields containing '\r' are wrapped and
double-quoted via the existing replace logic; ensure the function still returns
the sanitized value when no quoting is needed.
- Around line 68-70: The CSV Blob is created without a UTF-8 BOM which causes
mojibake in Excel for Japanese text; in downloadCsv, prepend the UTF-8 BOM (e.g.
'\uFEFF' or the byte sequence 0xEF,0xBB,0xBF) to the csvContent before
constructing the Blob (the code creating `const blob = new Blob([csvContent],
...)`), so replace that creation to include the BOM and then continue using
URL.createObjectURL(blob) and the existing filename/path logic.
---
Nitpick comments:
In `@src/utils/csvExport.ts`:
- Around line 70-77: Wrap the DOM/URL cleanup in a try/finally: after creating
url via URL.createObjectURL(blob) and the anchor element a
(document.createElement('a')), call document.body.appendChild(a) and perform
a.click() inside a try block, and in the finally ensure
document.body.removeChild(a) (only if appended) and URL.revokeObjectURL(url) are
always executed; this guarantees the temporary anchor and object URL (url, a,
URL.createObjectURL, URL.revokeObjectURL, a.click(),
document.body.appendChild/removeChild) are cleaned up even if a.click() or other
steps throw.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c8649632-41ef-4a06-8bc8-ebe66cc3513f
📒 Files selected for processing (3)
src/pages/SaleList.tsxsrc/utils/csvExport.test.tssrc/utils/csvExport.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pages/SaleList.tsx
- src/utils/csvExport.test.ts
売上明細をCSVファイルとしてダウンロードできる機能を追加。
日別表示時は日付ごと、カタログ別表示時はカタログごとにエクスポート可能。
CSVカラム: 日時, カタログ名, 商品名, 単価, 数量, 小計
https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
Summary by CodeRabbit
New Features
Tests