From d23db9ff84fbea9090dbba7b777613557c638196 Mon Sep 17 00:00:00 2001 From: Manas Dutta <122201926+manasdutta04@users.noreply.github.com> Date: Sun, 10 Aug 2025 20:14:34 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20data=20export=20functionality?= =?UTF-8?q?=20for=20GitHub=20issues=20and=20PRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/EXPORT_FEATURE.md | 108 +++++++++++++++++++++ src/components/ExportButton.tsx | 122 ++++++++++++++++++++++++ src/hooks/useGitHubData.ts | 9 +- src/pages/Tracker/Tracker.tsx | 77 +++++++++------ src/utils/__tests__/exportUtils.test.ts | 84 ++++++++++++++++ src/utils/exportUtils.ts | 86 +++++++++++++++++ 6 files changed, 457 insertions(+), 29 deletions(-) create mode 100644 docs/EXPORT_FEATURE.md create mode 100644 src/components/ExportButton.tsx create mode 100644 src/utils/__tests__/exportUtils.test.ts create mode 100644 src/utils/exportUtils.ts diff --git a/docs/EXPORT_FEATURE.md b/docs/EXPORT_FEATURE.md new file mode 100644 index 0000000..1aa4733 --- /dev/null +++ b/docs/EXPORT_FEATURE.md @@ -0,0 +1,108 @@ +# 📊 Data Export Feature + +## Overview +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. + +## Features + +### 🔄 Export Formats +- **CSV Format**: Spreadsheet-compatible format perfect for Excel, Google Sheets, or data analysis tools +- **JSON Format**: Developer-friendly format ideal for programmatic processing or API integration + +### 📋 Export Options +1. **Current Tab Export**: Export only the currently viewed data (Issues or Pull Requests) +2. **Export All**: Export both issues and pull requests in a single file +3. **Filtered Export**: Export respects all active filters (search, date range, repository, state) + +### 📁 File Structure + +#### CSV Export Columns +- ID: GitHub item ID +- Title: Issue/PR title +- State: Current state (open, closed, merged) +- Type: Issue or Pull Request +- Repository: Repository name +- Author: GitHub username of the author +- Labels: Comma-separated list of labels +- Created Date: Creation date in local format +- URL: Direct link to the GitHub item + +#### JSON Export Structure +```json +[ + { + "id": 123456, + "title": "Fix authentication bug", + "state": "closed", + "type": "Issue", + "repository": "github-tracker", + "author": "username", + "labels": ["bug", "authentication"], + "createdDate": "2024-01-15T10:30:00Z", + "url": "https://github.com/user/repo/issues/123" + } +] +``` + +## Usage + +### Basic Export +1. Navigate to the Tracker page +2. Enter your GitHub username and token +3. Click "Fetch Data" to load your GitHub activity +4. Click the "Export" button next to the state filter +5. Choose your preferred format (CSV or JSON) +6. The file will be automatically downloaded + +### Export All Data +1. After loading data, look for the "Export" button in the filters section +2. This exports both issues and pull requests combined +3. Choose your format and download + +### Export with Filters +1. Apply any combination of filters: + - Search by title + - Filter by repository + - Set date range + - Select state (open/closed/merged) +2. Click "Export" to download only the filtered results + +## File Naming Convention +Files are automatically named using the pattern: +`github-{username}-{type}-{date}.{format}` + +Examples: +- `github-johndoe-issues-2024-01-15.csv` +- `github-johndoe-prs-2024-01-15.json` +- `github-johndoe-all-2024-01-15.csv` + +## Technical Implementation + +### Components +- `ExportButton.tsx`: Main export component with dropdown menu +- `exportUtils.ts`: Utility functions for data processing and file generation + +### Key Functions +- `exportToCSV()`: Converts data to CSV format and triggers download +- `exportToJSON()`: Converts data to JSON format and triggers download +- `generateFilename()`: Creates standardized filenames +- `downloadFile()`: Handles browser download functionality + +### Error Handling +- Empty data validation +- Format-specific error handling +- User-friendly error messages via toast notifications +- Success confirmations + +## Browser Compatibility +- Modern browsers with Blob API support +- File download functionality +- No server-side processing required + +## Future Enhancements +- [ ] Excel (.xlsx) format support +- [ ] Custom column selection for CSV +- [ ] Scheduled exports +- [ ] Email export functionality +- [ ] Export templates +- [ ] Bulk repository analysis export \ No newline at end of file diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx new file mode 100644 index 0000000..5efec9e --- /dev/null +++ b/src/components/ExportButton.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { + Button, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Divider, + Box, + Typography +} from '@mui/material'; +import { Download, FileText, Code } from 'lucide-react'; +import toast from 'react-hot-toast'; +import { exportToCSV, exportToJSON, generateFilename } from '../utils/exportUtils'; + +interface GitHubItem { + id: number; + title: string; + state: string; + created_at: string; + pull_request?: { merged_at: string | null }; + repository_url: string; + html_url: string; + user?: { login: string }; + labels?: Array<{ name: string }>; +} + +interface ExportButtonProps { + data: GitHubItem[]; + username: string; + type: 'issues' | 'prs' | 'all'; + disabled?: boolean; +} + +const ExportButton: React.FC = ({ + data, + username, + type, + disabled = false +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleExport = (format: 'csv' | 'json') => { + try { + const filename = generateFilename(username, type, format); + + if (format === 'csv') { + exportToCSV(data, filename); + } else { + exportToJSON(data, filename); + } + + toast.success(`Successfully exported ${data.length} items as ${format.toUpperCase()}`); + } catch (error) { + toast.error('Failed to export data. Please try again.'); + console.error('Export error:', error); + } + + handleClose(); + }; + + return ( + + + + + + + Export {data.length} items + + + + + + handleExport('csv')}> + + + + + + + handleExport('json')}> + + + + + + + + ); +}; + +export default ExportButton; \ No newline at end of file diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..dd6ed55 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -19,8 +19,15 @@ export const useGitHubData = (getOctokit: () => any) => { page, }); + // Enhance items with additional data for export + const enhancedItems = response.data.items.map((item: any) => ({ + ...item, + user: item.user || { login: 'Unknown' }, + labels: item.labels || [] + })); + return { - items: response.data.items, + items: enhancedItems, total: response.data.total_count, }; }; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 7d949ec..9f03c05 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -32,6 +32,7 @@ import { import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { useGitHubData } from "../../hooks/useGitHubData"; +import ExportButton from "../../components/ExportButton"; const ROWS_PER_PAGE = 10; @@ -184,7 +185,7 @@ const Home: React.FC = () => { {/* Filters */} - + { InputLabelProps={{ shrink: true }} sx={{ minWidth: 150 }} /> + + {/* Export All Button */} + {(issues.length > 0 || prs.length > 0) && ( + + )} - {/* Tabs + State Filter */} + {/* Tabs + State Filter + Export */} { - - State - - + + + + + + State + + + {(authError || dataError) && ( diff --git a/src/utils/__tests__/exportUtils.test.ts b/src/utils/__tests__/exportUtils.test.ts new file mode 100644 index 0000000..e6ef5ed --- /dev/null +++ b/src/utils/__tests__/exportUtils.test.ts @@ -0,0 +1,84 @@ +import { exportToCSV, exportToJSON, generateFilename } from '../exportUtils'; + +// Mock data for testing +const mockData = [ + { + id: 1, + title: 'Test Issue', + state: 'open', + created_at: '2024-01-01T00:00:00Z', + repository_url: 'https://api.github.com/repos/user/repo', + html_url: 'https://github.com/user/repo/issues/1', + user: { login: 'testuser' }, + labels: [{ name: 'bug' }, { name: 'help wanted' }] + }, + { + id: 2, + title: 'Test PR', + state: 'closed', + created_at: '2024-01-02T00:00:00Z', + pull_request: { merged_at: '2024-01-03T00:00:00Z' }, + repository_url: 'https://api.github.com/repos/user/repo', + html_url: 'https://github.com/user/repo/pull/2', + user: { login: 'testuser' }, + labels: [{ name: 'enhancement' }] + } +]; + +// Mock DOM methods +Object.defineProperty(global, 'URL', { + value: { + createObjectURL: jest.fn(() => 'mock-url'), + revokeObjectURL: jest.fn(), + }, +}); + +Object.defineProperty(document, 'createElement', { + value: jest.fn(() => ({ + href: '', + download: '', + click: jest.fn(), + })), +}); + +Object.defineProperty(document.body, 'appendChild', { + value: jest.fn(), +}); + +Object.defineProperty(document.body, 'removeChild', { + value: jest.fn(), +}); + +describe('Export Utils', () => { + describe('generateFilename', () => { + it('should generate correct filename for issues', () => { + const filename = generateFilename('testuser', 'issues', 'csv'); + expect(filename).toMatch(/github-testuser-issues-\d{4}-\d{2}-\d{2}\.csv/); + }); + + it('should generate correct filename for PRs', () => { + const filename = generateFilename('testuser', 'prs', 'json'); + expect(filename).toMatch(/github-testuser-prs-\d{4}-\d{2}-\d{2}\.json/); + }); + }); + + describe('exportToCSV', () => { + it('should throw error for empty data', () => { + expect(() => exportToCSV([], 'test.csv')).toThrow('No data to export'); + }); + + it('should not throw error for valid data', () => { + expect(() => exportToCSV(mockData, 'test.csv')).not.toThrow(); + }); + }); + + describe('exportToJSON', () => { + it('should throw error for empty data', () => { + expect(() => exportToJSON([], 'test.json')).toThrow('No data to export'); + }); + + it('should not throw error for valid data', () => { + expect(() => exportToJSON(mockData, 'test.json')).not.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts new file mode 100644 index 0000000..10ac5fe --- /dev/null +++ b/src/utils/exportUtils.ts @@ -0,0 +1,86 @@ +interface GitHubItem { + id: number; + title: string; + state: string; + created_at: string; + pull_request?: { merged_at: string | null }; + repository_url: string; + html_url: string; + user?: { login: string }; + labels?: Array<{ name: string }>; +} + +export const exportToCSV = (data: GitHubItem[], filename: string) => { + if (!data.length) { + throw new Error('No data to export'); + } + + const headers = [ + 'ID', + 'Title', + 'State', + 'Type', + 'Repository', + 'Author', + 'Labels', + 'Created Date', + 'URL' + ]; + + const csvContent = [ + headers.join(','), + ...data.map(item => [ + item.id, + `"${item.title.replace(/"/g, '""')}"`, // Escape quotes in title + item.pull_request?.merged_at ? 'merged' : item.state, + item.pull_request ? 'Pull Request' : 'Issue', + `"${item.repository_url.split('/').slice(-1)[0]}"`, + `"${item.user?.login || 'N/A'}"`, + `"${item.labels?.map(label => label.name).join('; ') || 'None'}"`, + new Date(item.created_at).toLocaleDateString(), + item.html_url + ].join(',')) + ].join('\n'); + + downloadFile(csvContent, filename, 'text/csv'); +}; + +export const exportToJSON = (data: GitHubItem[], filename: string) => { + if (!data.length) { + throw new Error('No data to export'); + } + + const exportData = data.map(item => ({ + id: item.id, + title: item.title, + state: item.pull_request?.merged_at ? 'merged' : item.state, + type: item.pull_request ? 'Pull Request' : 'Issue', + repository: item.repository_url.split('/').slice(-1)[0], + author: item.user?.login || 'N/A', + labels: item.labels?.map(label => label.name) || [], + createdDate: item.created_at, + url: item.html_url + })); + + const jsonContent = JSON.stringify(exportData, null, 2); + downloadFile(jsonContent, filename, 'application/json'); +}; + +const downloadFile = (content: string, filename: string, mimeType: string) => { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(url); +}; + +export const generateFilename = (username: string, type: 'issues' | 'prs' | 'all', format: 'csv' | 'json') => { + const timestamp = new Date().toISOString().split('T')[0]; + return `github-${username}-${type}-${timestamp}.${format}`; +}; \ No newline at end of file