Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions docs/EXPORT_FEATURE.md
Original file line number Diff line number Diff line change
@@ -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
122 changes: 122 additions & 0 deletions src/components/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -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<ExportButtonProps> = ({
data,
username,
type,
disabled = false
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
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 (
<Box>
<Button
variant="outlined"
startIcon={<Download size={16} />}
onClick={handleClick}
disabled={disabled || data.length === 0}
sx={{ minWidth: 120 }}
>
Export
</Button>

<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem disabled sx={{ opacity: 0.7 }}>
<Typography variant="caption" color="text.secondary">
Export {data.length} items
</Typography>
</MenuItem>

<Divider />

<MenuItem onClick={() => handleExport('csv')}>
<ListItemIcon>
<FileText size={16} />
</ListItemIcon>
<ListItemText primary="CSV Format" secondary="Spreadsheet compatible" />
</MenuItem>

<MenuItem onClick={() => handleExport('json')}>
<ListItemIcon>
<Code size={16} />
</ListItemIcon>
<ListItemText primary="JSON Format" secondary="Developer friendly" />
</MenuItem>
</Menu>
</Box>
);
};

export default ExportButton;
9 changes: 8 additions & 1 deletion src/hooks/useGitHubData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Expand Down
77 changes: 49 additions & 28 deletions src/pages/Tracker/Tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

{/* Filters */}
<Box sx={{ mb: 2, display: "flex", flexWrap: "wrap", gap: 2 }}>
<Box sx={{ mb: 2, display: "flex", flexWrap: "wrap", gap: 2, alignItems: "center" }}>
<TextField
label="Search Title"
value={searchTitle}
Expand Down Expand Up @@ -213,9 +214,19 @@ const Home: React.FC = () => {
InputLabelProps={{ shrink: true }}
sx={{ minWidth: 150 }}
/>

{/* Export All Button */}
{(issues.length > 0 || prs.length > 0) && (
<ExportButton
data={[...filterData(issues, issueFilter), ...filterData(prs, prFilter)]}
username={username}
type="all"
disabled={loading || !username}
/>
)}
</Box>

{/* Tabs + State Filter */}
{/* Tabs + State Filter + Export */}
<Box
sx={{
display: "flex",
Expand All @@ -237,32 +248,42 @@ const Home: React.FC = () => {
<Tab label={`Issues (${totalIssues})`} />
<Tab label={`Pull Requests (${totalPrs})`} />
</Tabs>
<FormControl sx={{ minWidth: 150 }}>
<InputLabel sx={{ fontSize: "14px" }}>State</InputLabel>
<Select
value={tab === 0 ? issueFilter : prFilter}
onChange={(e) =>
tab === 0
? setIssueFilter(e.target.value)
: setPrFilter(e.target.value)
}
label="State"
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
borderRadius: "4px",
"& .MuiSelect-select": { padding: "10px" },
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.main,
},
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="open">Open</MenuItem>
<MenuItem value="closed">Closed</MenuItem>
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
</Select>
</FormControl>

<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
<ExportButton
data={currentFilteredData}
username={username}
type={tab === 0 ? 'issues' : 'prs'}
disabled={loading || !username}
/>

<FormControl sx={{ minWidth: 150 }}>
<InputLabel sx={{ fontSize: "14px" }}>State</InputLabel>
<Select
value={tab === 0 ? issueFilter : prFilter}
onChange={(e) =>
tab === 0
? setIssueFilter(e.target.value)
: setPrFilter(e.target.value)
}
label="State"
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
borderRadius: "4px",
"& .MuiSelect-select": { padding: "10px" },
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.main,
},
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="open">Open</MenuItem>
<MenuItem value="closed">Closed</MenuItem>
{tab === 1 && <MenuItem value="merged">Merged</MenuItem>}
</Select>
</FormControl>
</Box>
</Box>

{(authError || dataError) && (
Expand Down
Loading