Skip to content

Commit d23db9f

Browse files
committed
✨ Add data export functionality for GitHub issues and PRs
- Add CSV and JSON export formats - Create reusable ExportButton component with dropdown menu - Implement export utilities with proper error handling - Add export options for current tab and all data - Support filtered data export (respects search, date, repo filters) - Include success/error notifications with react-hot-toast - Add automatic filename generation with timestamps - Enhance GitHub data hook to include user and labels info - Add comprehensive documentation and basic tests - Improve user experience with disabled states and item counts Fixes: Data export functionality for better data analysis and backup
1 parent 988166d commit d23db9f

6 files changed

Lines changed: 457 additions & 29 deletions

File tree

docs/EXPORT_FEATURE.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# 📊 Data Export Feature
2+
3+
## Overview
4+
The Data Export feature allows users to export their GitHub issues and pull requests data in CSV and JSON formats for further analysis, reporting, or backup purposes.
5+
6+
## Features
7+
8+
### 🔄 Export Formats
9+
- **CSV Format**: Spreadsheet-compatible format perfect for Excel, Google Sheets, or data analysis tools
10+
- **JSON Format**: Developer-friendly format ideal for programmatic processing or API integration
11+
12+
### 📋 Export Options
13+
1. **Current Tab Export**: Export only the currently viewed data (Issues or Pull Requests)
14+
2. **Export All**: Export both issues and pull requests in a single file
15+
3. **Filtered Export**: Export respects all active filters (search, date range, repository, state)
16+
17+
### 📁 File Structure
18+
19+
#### CSV Export Columns
20+
- ID: GitHub item ID
21+
- Title: Issue/PR title
22+
- State: Current state (open, closed, merged)
23+
- Type: Issue or Pull Request
24+
- Repository: Repository name
25+
- Author: GitHub username of the author
26+
- Labels: Comma-separated list of labels
27+
- Created Date: Creation date in local format
28+
- URL: Direct link to the GitHub item
29+
30+
#### JSON Export Structure
31+
```json
32+
[
33+
{
34+
"id": 123456,
35+
"title": "Fix authentication bug",
36+
"state": "closed",
37+
"type": "Issue",
38+
"repository": "github-tracker",
39+
"author": "username",
40+
"labels": ["bug", "authentication"],
41+
"createdDate": "2024-01-15T10:30:00Z",
42+
"url": "https://github.com/user/repo/issues/123"
43+
}
44+
]
45+
```
46+
47+
## Usage
48+
49+
### Basic Export
50+
1. Navigate to the Tracker page
51+
2. Enter your GitHub username and token
52+
3. Click "Fetch Data" to load your GitHub activity
53+
4. Click the "Export" button next to the state filter
54+
5. Choose your preferred format (CSV or JSON)
55+
6. The file will be automatically downloaded
56+
57+
### Export All Data
58+
1. After loading data, look for the "Export" button in the filters section
59+
2. This exports both issues and pull requests combined
60+
3. Choose your format and download
61+
62+
### Export with Filters
63+
1. Apply any combination of filters:
64+
- Search by title
65+
- Filter by repository
66+
- Set date range
67+
- Select state (open/closed/merged)
68+
2. Click "Export" to download only the filtered results
69+
70+
## File Naming Convention
71+
Files are automatically named using the pattern:
72+
`github-{username}-{type}-{date}.{format}`
73+
74+
Examples:
75+
- `github-johndoe-issues-2024-01-15.csv`
76+
- `github-johndoe-prs-2024-01-15.json`
77+
- `github-johndoe-all-2024-01-15.csv`
78+
79+
## Technical Implementation
80+
81+
### Components
82+
- `ExportButton.tsx`: Main export component with dropdown menu
83+
- `exportUtils.ts`: Utility functions for data processing and file generation
84+
85+
### Key Functions
86+
- `exportToCSV()`: Converts data to CSV format and triggers download
87+
- `exportToJSON()`: Converts data to JSON format and triggers download
88+
- `generateFilename()`: Creates standardized filenames
89+
- `downloadFile()`: Handles browser download functionality
90+
91+
### Error Handling
92+
- Empty data validation
93+
- Format-specific error handling
94+
- User-friendly error messages via toast notifications
95+
- Success confirmations
96+
97+
## Browser Compatibility
98+
- Modern browsers with Blob API support
99+
- File download functionality
100+
- No server-side processing required
101+
102+
## Future Enhancements
103+
- [ ] Excel (.xlsx) format support
104+
- [ ] Custom column selection for CSV
105+
- [ ] Scheduled exports
106+
- [ ] Email export functionality
107+
- [ ] Export templates
108+
- [ ] Bulk repository analysis export

src/components/ExportButton.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Button,
4+
Menu,
5+
MenuItem,
6+
ListItemIcon,
7+
ListItemText,
8+
Divider,
9+
Box,
10+
Typography
11+
} from '@mui/material';
12+
import { Download, FileText, Code } from 'lucide-react';
13+
import toast from 'react-hot-toast';
14+
import { exportToCSV, exportToJSON, generateFilename } from '../utils/exportUtils';
15+
16+
interface GitHubItem {
17+
id: number;
18+
title: string;
19+
state: string;
20+
created_at: string;
21+
pull_request?: { merged_at: string | null };
22+
repository_url: string;
23+
html_url: string;
24+
user?: { login: string };
25+
labels?: Array<{ name: string }>;
26+
}
27+
28+
interface ExportButtonProps {
29+
data: GitHubItem[];
30+
username: string;
31+
type: 'issues' | 'prs' | 'all';
32+
disabled?: boolean;
33+
}
34+
35+
const ExportButton: React.FC<ExportButtonProps> = ({
36+
data,
37+
username,
38+
type,
39+
disabled = false
40+
}) => {
41+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
42+
const open = Boolean(anchorEl);
43+
44+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
45+
setAnchorEl(event.currentTarget);
46+
};
47+
48+
const handleClose = () => {
49+
setAnchorEl(null);
50+
};
51+
52+
const handleExport = (format: 'csv' | 'json') => {
53+
try {
54+
const filename = generateFilename(username, type, format);
55+
56+
if (format === 'csv') {
57+
exportToCSV(data, filename);
58+
} else {
59+
exportToJSON(data, filename);
60+
}
61+
62+
toast.success(`Successfully exported ${data.length} items as ${format.toUpperCase()}`);
63+
} catch (error) {
64+
toast.error('Failed to export data. Please try again.');
65+
console.error('Export error:', error);
66+
}
67+
68+
handleClose();
69+
};
70+
71+
return (
72+
<Box>
73+
<Button
74+
variant="outlined"
75+
startIcon={<Download size={16} />}
76+
onClick={handleClick}
77+
disabled={disabled || data.length === 0}
78+
sx={{ minWidth: 120 }}
79+
>
80+
Export
81+
</Button>
82+
83+
<Menu
84+
anchorEl={anchorEl}
85+
open={open}
86+
onClose={handleClose}
87+
anchorOrigin={{
88+
vertical: 'bottom',
89+
horizontal: 'left',
90+
}}
91+
transformOrigin={{
92+
vertical: 'top',
93+
horizontal: 'left',
94+
}}
95+
>
96+
<MenuItem disabled sx={{ opacity: 0.7 }}>
97+
<Typography variant="caption" color="text.secondary">
98+
Export {data.length} items
99+
</Typography>
100+
</MenuItem>
101+
102+
<Divider />
103+
104+
<MenuItem onClick={() => handleExport('csv')}>
105+
<ListItemIcon>
106+
<FileText size={16} />
107+
</ListItemIcon>
108+
<ListItemText primary="CSV Format" secondary="Spreadsheet compatible" />
109+
</MenuItem>
110+
111+
<MenuItem onClick={() => handleExport('json')}>
112+
<ListItemIcon>
113+
<Code size={16} />
114+
</ListItemIcon>
115+
<ListItemText primary="JSON Format" secondary="Developer friendly" />
116+
</MenuItem>
117+
</Menu>
118+
</Box>
119+
);
120+
};
121+
122+
export default ExportButton;

src/hooks/useGitHubData.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ export const useGitHubData = (getOctokit: () => any) => {
1919
page,
2020
});
2121

22+
// Enhance items with additional data for export
23+
const enhancedItems = response.data.items.map((item: any) => ({
24+
...item,
25+
user: item.user || { login: 'Unknown' },
26+
labels: item.labels || []
27+
}));
28+
2229
return {
23-
items: response.data.items,
30+
items: enhancedItems,
2431
total: response.data.total_count,
2532
};
2633
};

src/pages/Tracker/Tracker.tsx

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import { useTheme } from "@mui/material/styles";
3333
import { useGitHubAuth } from "../../hooks/useGitHubAuth";
3434
import { useGitHubData } from "../../hooks/useGitHubData";
35+
import ExportButton from "../../components/ExportButton";
3536

3637
const ROWS_PER_PAGE = 10;
3738

@@ -184,7 +185,7 @@ const Home: React.FC = () => {
184185
</Paper>
185186

186187
{/* Filters */}
187-
<Box sx={{ mb: 2, display: "flex", flexWrap: "wrap", gap: 2 }}>
188+
<Box sx={{ mb: 2, display: "flex", flexWrap: "wrap", gap: 2, alignItems: "center" }}>
188189
<TextField
189190
label="Search Title"
190191
value={searchTitle}
@@ -213,9 +214,19 @@ const Home: React.FC = () => {
213214
InputLabelProps={{ shrink: true }}
214215
sx={{ minWidth: 150 }}
215216
/>
217+
218+
{/* Export All Button */}
219+
{(issues.length > 0 || prs.length > 0) && (
220+
<ExportButton
221+
data={[...filterData(issues, issueFilter), ...filterData(prs, prFilter)]}
222+
username={username}
223+
type="all"
224+
disabled={loading || !username}
225+
/>
226+
)}
216227
</Box>
217228

218-
{/* Tabs + State Filter */}
229+
{/* Tabs + State Filter + Export */}
219230
<Box
220231
sx={{
221232
display: "flex",
@@ -237,32 +248,42 @@ const Home: React.FC = () => {
237248
<Tab label={`Issues (${totalIssues})`} />
238249
<Tab label={`Pull Requests (${totalPrs})`} />
239250
</Tabs>
240-
<FormControl sx={{ minWidth: 150 }}>
241-
<InputLabel sx={{ fontSize: "14px" }}>State</InputLabel>
242-
<Select
243-
value={tab === 0 ? issueFilter : prFilter}
244-
onChange={(e) =>
245-
tab === 0
246-
? setIssueFilter(e.target.value)
247-
: setPrFilter(e.target.value)
248-
}
249-
label="State"
250-
sx={{
251-
backgroundColor: theme.palette.background.paper,
252-
color: theme.palette.text.primary,
253-
borderRadius: "4px",
254-
"& .MuiSelect-select": { padding: "10px" },
255-
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
256-
borderColor: theme.palette.primary.main,
257-
},
258-
}}
259-
>
260-
<MenuItem value="all">All</MenuItem>
261-
<MenuItem value="open">Open</MenuItem>
262-
<MenuItem value="closed">Closed</MenuItem>
263-
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
264-
</Select>
265-
</FormControl>
251+
252+
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
253+
<ExportButton
254+
data={currentFilteredData}
255+
username={username}
256+
type={tab === 0 ? 'issues' : 'prs'}
257+
disabled={loading || !username}
258+
/>
259+
260+
<FormControl sx={{ minWidth: 150 }}>
261+
<InputLabel sx={{ fontSize: "14px" }}>State</InputLabel>
262+
<Select
263+
value={tab === 0 ? issueFilter : prFilter}
264+
onChange={(e) =>
265+
tab === 0
266+
? setIssueFilter(e.target.value)
267+
: setPrFilter(e.target.value)
268+
}
269+
label="State"
270+
sx={{
271+
backgroundColor: theme.palette.background.paper,
272+
color: theme.palette.text.primary,
273+
borderRadius: "4px",
274+
"& .MuiSelect-select": { padding: "10px" },
275+
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
276+
borderColor: theme.palette.primary.main,
277+
},
278+
}}
279+
>
280+
<MenuItem value="all">All</MenuItem>
281+
<MenuItem value="open">Open</MenuItem>
282+
<MenuItem value="closed">Closed</MenuItem>
283+
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
284+
</Select>
285+
</FormControl>
286+
</Box>
266287
</Box>
267288

268289
{(authError || dataError) && (

0 commit comments

Comments
 (0)