Skip to content

Commit ef163d2

Browse files
authored
Merge pull request #1113 from Gahan-Shetty/fix/csv-export-toasts
Approved after code review and security audit. Clean PR with toast notifications, memory leak fixes, and filter-aware exports.
2 parents b368089 + 4ba1e3a commit ef163d2

4 files changed

Lines changed: 170 additions & 122 deletions

File tree

frontend/src/pages/Holdings.tsx

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { cn, makeFormatCurrency, sanitizeCSV } from '@/lib/utils'
2929
import { useAuthStore } from '@/stores/authStore'
3030
import { onModeChange } from '@/stores/themeStore'
3131
import type { Holding, HoldingsStats } from '@/types/trading'
32+
import { showToast } from '@/utils/toast'
3233

3334
function formatPercent(value: number): string {
3435
return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`
@@ -151,36 +152,47 @@ export default function Holdings() {
151152
}, [fetchHoldings])
152153

153154
const exportToCSV = () => {
154-
const headers = [
155-
'Symbol',
156-
'Exchange',
157-
'Quantity',
158-
'Avg Price',
159-
'LTP',
160-
'Product',
161-
'P&L',
162-
'P&L %',
163-
]
164-
const rows = enhancedHoldings.map((h) => [
165-
sanitizeCSV(h.symbol),
166-
sanitizeCSV(h.exchange),
167-
sanitizeCSV(h.quantity),
168-
sanitizeCSV(h.average_price),
169-
sanitizeCSV(h.ltp),
170-
sanitizeCSV(h.product),
171-
sanitizeCSV(h.pnl),
172-
sanitizeCSV(h.pnlpercent),
173-
])
155+
if (enhancedHoldings.length === 0) {
156+
showToast.error('No data to export', 'system')
157+
return
158+
}
159+
160+
try {
161+
const headers = [
162+
'Symbol',
163+
'Exchange',
164+
'Quantity',
165+
'Avg Price',
166+
'LTP',
167+
'Product',
168+
'P&L',
169+
'P&L %',
170+
]
171+
const rows = enhancedHoldings.map((h) => [
172+
sanitizeCSV(h.symbol),
173+
sanitizeCSV(h.exchange),
174+
sanitizeCSV(h.quantity),
175+
sanitizeCSV(h.average_price),
176+
sanitizeCSV(h.ltp),
177+
sanitizeCSV(h.product),
178+
sanitizeCSV(h.pnl),
179+
sanitizeCSV(h.pnlpercent),
180+
])
174181

175-
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
176-
const blob = new Blob([csv], { type: 'text/csv' })
177-
const url = URL.createObjectURL(blob)
178-
const a = document.createElement('a')
179-
a.href = url
180-
a.download = `holdings_${new Date().toISOString().split('T')[0]}.csv`
181-
a.click()
182-
// Revoke the object URL to free memory
183-
URL.revokeObjectURL(url)
182+
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
183+
const blob = new Blob([csv], { type: 'text/csv' })
184+
const url = URL.createObjectURL(blob)
185+
const a = document.createElement('a')
186+
a.href = url
187+
const filename = `holdings_${new Date().toISOString().split('T')[0]}.csv`
188+
a.download = filename
189+
a.click()
190+
// Revoke the object URL to free memory
191+
URL.revokeObjectURL(url)
192+
showToast.success(`Downloaded ${filename}`, 'clipboard')
193+
} catch {
194+
showToast.error('Failed to export CSV', 'system')
195+
}
184196
}
185197

186198
const isProfit = (value: number) => value >= 0

frontend/src/pages/OrderBook.tsx

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -251,40 +251,52 @@ export default function OrderBook() {
251251
}
252252

253253
const exportToCSV = () => {
254-
const headers = [
255-
'Symbol',
256-
'Exchange',
257-
'Action',
258-
'Qty',
259-
'Price',
260-
'Trigger',
261-
'Type',
262-
'Product',
263-
'Order ID',
264-
'Status',
265-
'Time',
266-
]
267-
const rows = orders.map((o) => [
268-
sanitizeCSV(o.symbol),
269-
sanitizeCSV(o.exchange),
270-
sanitizeCSV(o.action),
271-
sanitizeCSV(o.quantity),
272-
sanitizeCSV(o.price),
273-
sanitizeCSV(o.trigger_price),
274-
sanitizeCSV(o.pricetype),
275-
sanitizeCSV(o.product),
276-
sanitizeCSV(o.orderid),
277-
sanitizeCSV(o.order_status),
278-
sanitizeCSV(o.timestamp),
279-
])
280-
281-
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
282-
const blob = new Blob([csv], { type: 'text/csv' })
283-
const url = URL.createObjectURL(blob)
284-
const a = document.createElement('a')
285-
a.href = url
286-
a.download = `orderbook_${new Date().toISOString().split('T')[0]}.csv`
287-
a.click()
254+
if (filteredOrders.length === 0) {
255+
showToast.error('No data to export', 'system')
256+
return
257+
}
258+
259+
try {
260+
const headers = [
261+
'Symbol',
262+
'Exchange',
263+
'Action',
264+
'Qty',
265+
'Price',
266+
'Trigger',
267+
'Type',
268+
'Product',
269+
'Order ID',
270+
'Status',
271+
'Time',
272+
]
273+
const rows = filteredOrders.map((o) => [
274+
sanitizeCSV(o.symbol),
275+
sanitizeCSV(o.exchange),
276+
sanitizeCSV(o.action),
277+
sanitizeCSV(o.quantity),
278+
sanitizeCSV(o.price),
279+
sanitizeCSV(o.trigger_price),
280+
sanitizeCSV(o.pricetype),
281+
sanitizeCSV(o.product),
282+
sanitizeCSV(o.orderid),
283+
sanitizeCSV(o.order_status),
284+
sanitizeCSV(o.timestamp),
285+
])
286+
287+
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
288+
const blob = new Blob([csv], { type: 'text/csv' })
289+
const url = URL.createObjectURL(blob)
290+
const a = document.createElement('a')
291+
a.href = url
292+
const filename = `orderbook_${new Date().toISOString().split('T')[0]}.csv`
293+
a.download = filename
294+
a.click()
295+
URL.revokeObjectURL(url)
296+
showToast.success(`Downloaded ${filename}`, 'clipboard')
297+
} catch {
298+
showToast.error('Failed to export CSV', 'system')
299+
}
288300
}
289301

290302
const openOrders = orders.filter((o) => o.order_status === 'open')

frontend/src/pages/Positions.tsx

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -468,36 +468,47 @@ export default function Positions() {
468468
}
469469

470470
const exportToCSV = () => {
471-
const headers = [
472-
'Symbol',
473-
'Exchange',
474-
'Product',
475-
'Quantity',
476-
'Avg Price',
477-
'LTP',
478-
'P&L',
479-
'P&L %',
480-
]
481-
const rows = filteredPositions.map((p) => [
482-
sanitizeCSV(p.symbol),
483-
sanitizeCSV(p.exchange),
484-
sanitizeCSV(p.product),
485-
sanitizeCSV(p.quantity),
486-
sanitizeCSV(p.average_price),
487-
sanitizeCSV(p.ltp),
488-
sanitizeCSV(p.pnl),
489-
sanitizeCSV(calculatePnlPercent(p)),
490-
])
491-
492-
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
493-
const blob = new Blob([csv], { type: 'text/csv' })
494-
const url = URL.createObjectURL(blob)
495-
const a = document.createElement('a')
496-
a.href = url
497-
a.download = `positions_${new Date().toISOString().split('T')[0]}.csv`
498-
a.click()
499-
// Revoke the object URL to free memory
500-
URL.revokeObjectURL(url)
471+
if (filteredPositions.length === 0) {
472+
showToast.error('No data to export', 'system')
473+
return
474+
}
475+
476+
try {
477+
const headers = [
478+
'Symbol',
479+
'Exchange',
480+
'Product',
481+
'Quantity',
482+
'Avg Price',
483+
'LTP',
484+
'P&L',
485+
'P&L %',
486+
]
487+
const rows = filteredPositions.map((p) => [
488+
sanitizeCSV(p.symbol),
489+
sanitizeCSV(p.exchange),
490+
sanitizeCSV(p.product),
491+
sanitizeCSV(p.quantity),
492+
sanitizeCSV(p.average_price),
493+
sanitizeCSV(p.ltp),
494+
sanitizeCSV(p.pnl),
495+
sanitizeCSV(calculatePnlPercent(p)),
496+
])
497+
498+
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
499+
const blob = new Blob([csv], { type: 'text/csv' })
500+
const url = URL.createObjectURL(blob)
501+
const a = document.createElement('a')
502+
a.href = url
503+
const filename = `positions_${new Date().toISOString().split('T')[0]}.csv`
504+
a.download = filename
505+
a.click()
506+
// Revoke the object URL to free memory
507+
URL.revokeObjectURL(url)
508+
showToast.success(`Downloaded ${filename}`, 'clipboard')
509+
} catch {
510+
showToast.error('Failed to export CSV', 'system')
511+
}
501512
}
502513

503514
const isProfit = (value: number) => value >= 0

frontend/src/pages/TradeBook.tsx

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Download, Loader2, RefreshCw, Settings2, TrendingDown, TrendingUp } fro
22
import { useCallback, useEffect, useMemo, useState } from 'react'
33
import { tradingApi } from '@/api/trading'
44
import { Badge } from '@/components/ui/badge'
5+
import { showToast } from '@/utils/toast'
56
import { Button } from '@/components/ui/button'
67
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
78
import {
@@ -131,36 +132,48 @@ export default function TradeBook() {
131132
}, [fetchTrades])
132133

133134
const exportToCSV = () => {
134-
const headers = [
135-
'Symbol',
136-
'Exchange',
137-
'Product',
138-
'Action',
139-
'Qty',
140-
'Price',
141-
'Trade Value',
142-
'Order ID',
143-
'Time',
144-
]
145-
const rows = trades.map((t) => [
146-
sanitizeCSV(t.symbol),
147-
sanitizeCSV(t.exchange),
148-
sanitizeCSV(t.product),
149-
sanitizeCSV(t.action),
150-
sanitizeCSV(t.quantity),
151-
sanitizeCSV(t.average_price),
152-
sanitizeCSV(t.trade_value),
153-
sanitizeCSV(t.orderid),
154-
sanitizeCSV(t.timestamp),
155-
])
135+
if (filteredTrades.length === 0) {
136+
showToast.error('No data to export', 'system')
137+
return
138+
}
156139

157-
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
158-
const blob = new Blob([csv], { type: 'text/csv' })
159-
const url = URL.createObjectURL(blob)
160-
const a = document.createElement('a')
161-
a.href = url
162-
a.download = `tradebook_${new Date().toISOString().split('T')[0]}.csv`
163-
a.click()
140+
try {
141+
const headers = [
142+
'Symbol',
143+
'Exchange',
144+
'Product',
145+
'Action',
146+
'Qty',
147+
'Price',
148+
'Trade Value',
149+
'Order ID',
150+
'Time',
151+
]
152+
const rows = filteredTrades.map((t) => [
153+
sanitizeCSV(t.symbol),
154+
sanitizeCSV(t.exchange),
155+
sanitizeCSV(t.product),
156+
sanitizeCSV(t.action),
157+
sanitizeCSV(t.quantity),
158+
sanitizeCSV(t.average_price),
159+
sanitizeCSV(t.trade_value),
160+
sanitizeCSV(t.orderid),
161+
sanitizeCSV(t.timestamp),
162+
])
163+
164+
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
165+
const blob = new Blob([csv], { type: 'text/csv' })
166+
const url = URL.createObjectURL(blob)
167+
const a = document.createElement('a')
168+
a.href = url
169+
const filename = `tradebook_${new Date().toISOString().split('T')[0]}.csv`
170+
a.download = filename
171+
a.click()
172+
URL.revokeObjectURL(url)
173+
showToast.success(`Downloaded ${filename}`, 'clipboard')
174+
} catch {
175+
showToast.error('Failed to export CSV', 'system')
176+
}
164177
}
165178

166179
const stats = {

0 commit comments

Comments
 (0)