From 27211b64ed3e9db43c1f6ff290aae082386b33c6 Mon Sep 17 00:00:00 2001 From: Chirag Bangera Date: Fri, 23 Oct 2026 19:15:35 -0400 Subject: [PATCH 1/4] chore: ignore .DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store From e2a8a02bda1e76dcdb27c4b625f4b3c4b4f65b36 Mon Sep 17 00:00:00 2001 From: Chirag Bangera Date: Fri, 23 Oct 2026 19:15:35 -0400 Subject: [PATCH 2/4] feat: add custom export file names Add optional per-file name fields under Advanced so each export can be saved under a fixed name of the user's choosing. Blank fields keep the default dated names. Names are remembered between popup sessions. --- README.md | 1 + content.js | 57 ++++++++++++++++++++++++++++++++++++------- popup.css | 29 ++++++++++++++++++++++ popup.html | 36 +++++++++++++++++++++++++++ popup.js | 41 ++++++++++++++++++++++++++++++- tests/content.test.js | 28 +++++++++++++++++++-- tests/popup.test.js | 34 +++++++++++++++++++++++--- 7 files changed, 210 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3d350eb..0d9a035 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The **Credit Karma Data Extractor** exports transaction history, net worth histo - **Instant Stop**: Cancel huge extractions immediately without losing data—what you've fetched is saved. - **Smart Export**: Automatically generates `All Data`, `Income Only`, and `Expenses Only` files. - **One-File BudgetLens Export**: The default BudgetLens Bundle combines transactions, both wealth histories, the current five-category breakdown, and available account sources in one versioned JSON file. +- **Custom File Names**: Optional Advanced setting to name each export file yourself (for example `transactions.csv` or `net_worth.csv`). Names are remembered, and blank fields keep the default dated names. - **Wealth History**: Export the full Net Worth and Investment graph series as separate, date-filtered CSV files. - **Current Net Worth Breakdown**: Export current Cash, Investments, Property, Credit cards, and Loans totals to `net_worth_breakdown_.csv`, plus available cash, investment, and property source balances to `wealth_accounts_.csv`. These snapshots are not filtered by the selected historical date range. diff --git a/content.js b/content.js index 164fd2b..94dbd95 100644 --- a/content.js +++ b/content.js @@ -1358,6 +1358,42 @@ function buildBudgetLensBundle({ }; } +const DEFAULT_EXPORT_BASE_NAMES = { + allTransactions: 'all_transactions', + income: 'income', + expenses: 'expenses', + netWorth: 'net_worth', + investments: 'investments', + netWorthBreakdown: 'net_worth_breakdown', + wealthAccounts: 'wealth_accounts', + budgetLensBundle: 'budgetlens' +}; + +/** + * Clean a user-supplied file name: drop path separators and characters that + * are invalid in file names, and any trailing .csv/.json the user typed. + */ +function sanitizeCustomFileName(name) { + return String(name ?? '') + .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '') + .trim() + .replace(/\.(csv|json)$/i, '') + .replace(/^\.+/, '') + .slice(0, 100) + .trim(); +} + +/** + * Build an export file name. A custom name for the export key is used as-is; + * otherwise the default name gets a date suffix. + */ +function buildExportFileName(key, extension, dateSuffix, customNames = {}) { + const custom = sanitizeCustomFileName(customNames?.[key]); + return custom + ? `${custom}.${extension}` + : `${DEFAULT_EXPORT_BASE_NAMES[key]}_${dateSuffix}.${extension}`; +} + function saveCSVToFile(csvData, fileName) { const blob = new Blob([csvData], { type: 'text/csv' }); const link = document.createElement('a'); @@ -1754,7 +1790,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } if (request.action === 'captureTransactions') { try { - const { startDate, endDate, csvTypes, fetchAccountNames = false, useApi = true, columns } = request; + const { startDate, endDate, csvTypes, fetchAccountNames = false, useApi = true, columns, fileNames = {} } = request; + const rangeSuffix = `${startDate}_to_${endDate}`; + const fileName = (key, extension, dateSuffix = rangeSuffix) => + buildExportFileName(key, extension, dateSuffix, fileNames); const needsTransactions = csvTypes.budgetLensBundle || csvTypes.allTransactions || @@ -1798,19 +1837,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else { if (csvTypes.allTransactions) { const allCsvData = convertToCSV(filteredTransactions, columns); - saveCSVToFile(allCsvData, `all_transactions_${startDate}_to_${endDate}.csv`); + saveCSVToFile(allCsvData, fileName('allTransactions', 'csv')); } if (csvTypes.income) { const creditTransactions = filteredTransactions.filter(transaction => transaction.transactionType === 'credit'); const creditCsvData = convertToCSV(creditTransactions, columns); - saveCSVToFile(creditCsvData, `income_${startDate}_to_${endDate}.csv`); + saveCSVToFile(creditCsvData, fileName('income', 'csv')); } if (csvTypes.expenses) { const debitTransactions = filteredTransactions.filter(transaction => transaction.transactionType === 'debit'); const debitCsvData = convertToCSV(debitTransactions, columns); - saveCSVToFile(debitCsvData, `expenses_${startDate}_to_${endDate}.csv`); + saveCSVToFile(debitCsvData, fileName('expenses', 'csv')); } } @@ -1818,7 +1857,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (csvTypes.netWorth || csvTypes.budgetLensBundle) { graphExports.push({ type: 'netWorth', - fileName: `net_worth_${startDate}_to_${endDate}.csv`, + fileName: fileName('netWorth', 'csv'), valueColumn: 'Net Worth', saveCSV: csvTypes.netWorth }); @@ -1826,7 +1865,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (csvTypes.investments || csvTypes.budgetLensBundle) { graphExports.push({ type: 'investments', - fileName: `investments_${startDate}_to_${endDate}.csv`, + fileName: fileName('investments', 'csv'), valueColumn: 'Investment Value', saveCSV: csvTypes.investments }); @@ -1867,7 +1906,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (saveSnapshotCSVs) { saveCSVToFile( convertNetWorthBreakdownToCSV(breakdownRows), - `net_worth_breakdown_${asOf.toISOString().slice(0, 10)}.csv` + fileName('netWorthBreakdown', 'csv', asOf.toISOString().slice(0, 10)) ); } @@ -1876,7 +1915,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (saveSnapshotCSVs) { saveCSVToFile( convertWealthAccountsToCSV(accountRows), - `wealth_accounts_${asOf.toISOString().slice(0, 10)}.csv` + fileName('wealthAccounts', 'csv', asOf.toISOString().slice(0, 10)) ); } } @@ -1897,7 +1936,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); saveJSONToFile( bundle, - `budgetlens_${startDate}_to_${endDate}.json` + fileName('budgetLensBundle', 'json') ); } diff --git a/popup.css b/popup.css index f6c0829..dd7b583 100644 --- a/popup.css +++ b/popup.css @@ -343,6 +343,35 @@ input:checked+.slider:before { box-shadow: none; } +.file-names { + margin-top: 12px; + font-size: 11px; +} + +.file-name-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.file-name-row span { + flex: 0 0 120px; + color: var(--text-secondary); +} + +.file-name-row input { + flex: 1; + min-width: 0; + padding: 4px 6px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font: inherit; +} + .secondary-btn { width: 100%; margin-top: 10px; diff --git a/popup.html b/popup.html index 666b998..f37e903 100644 --- a/popup.html +++ b/popup.html @@ -119,6 +119,42 @@

Transaction Columns

+
+ Custom file names + Optional. Leave blank for the default dated name; the extension is added for you. + + + + + + + + +