diff --git a/README.md b/README.md index 8655d6ad..901b37ef 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,19 @@ ## Features - Automatically fetches your Git activity, including commits, pull requests, issues, and code reviews. -- Currently supports GitHub, with plans to expand to other platforms +- **Multi-Platform Support**: GitHub, GitLab (NEW), with Gitea coming soon - Generates editable scrum updates based on your selected date range - Integrates directly with compose windows in Google Groups, Gmail, Yahoo Mail, and Outlook +- Platform selector for seamless switching between GitHub and GitLab + +## Roadmap + +### Upcoming Features 🚀 +- ✅ **GitLab Support** - Full GitLab project and merge request integration +- 🔜 **Gitea Support** - Add Gitea instance compatibility +- 📊 **Multi-SCM Analytics** - Unified dashboard for cross-platform activity +- 🔗 **Unified API Layer** - Abstract provider implementation for easier extensions +- 📈 **Advanced Analytics** - Commit trends, contribution graphs, team metrics ## How to install diff --git a/biome.json b/biome.json index 0e557ce7..1664e03d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 04597975..2bbcf113 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -432,5 +432,9 @@ "insertedInEmailNotification": { "message": "Report inserted into email!", "description": "Notification shown when report is successfully inserted into email." + }, + "invalidDateRangeError": { + "message": "Invalid date range. Please select a valid date range.", + "description": "Error shown when start date is after end date." } } \ No newline at end of file diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index aec629b7..991983bb 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -186,11 +186,37 @@ class GitLabHelper { } } + // Fetch commits from each project + let allCommits = []; + for (const project of allProjects) { + try { + const projectCommitsUrl = `${this.baseUrl}/projects/${project.id}/repository/commits?author_name=${username}&since=${startDate}T00:00:00Z&until=${endDate}T23:59:59Z&per_page=100&order_by=committed_date&sort=desc`; + const projectCommitsRes = await fetch(projectCommitsUrl, { headers }); + if (projectCommitsRes.ok) { + const projectCommits = await projectCommitsRes.json(); + allCommits = allCommits.concat( + projectCommits.map((commit) => ({ + ...commit, + project_id: project.id, + project_name: project.name, + project_url: project.web_url, + })), + ); + } + // Add small delay to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error fetching commits for project ${project.name}:`, error); + // Continue with other projects + } + } + const gitlabData = { user: users[0], projects: allProjects, mergeRequests: allMergeRequests, // use project-by-project response issues: allIssues, // use project-by-project response + commits: allCommits, // commits from all projects comments: [], // Empty array since we're not fetching comments }; // Cache the data @@ -277,6 +303,7 @@ class GitLabHelper { const processed = { mergeRequests: data.mergeRequests || [], issues: data.issues || [], + commits: data.commits || [], comments: data.comments || [], user: data.user, }; diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 9fe59537..4a0f8eef 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -90,6 +90,34 @@ function getYesterday() { return yesterday.toISOString().split('T')[0]; } +// Validate that start date is not after end date +function validateDateRange(startDate, endDate) { + if (!startDate || !endDate) { + return true; // Allow empty dates (will be handled by other validation) + } + + const start = new Date(startDate); + const end = new Date(endDate); + + // Check for invalid dates + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + return true; // Allow invalid dates to be handled by other validation + } + + return start <= end; +} + +// Show date validation error message +function showDateValidationError(message) { + // Use Materialize toast if available + if (typeof Materialize !== 'undefined' && Materialize.toast) { + Materialize.toast(message, 4000, 'red'); + } else { + // Fallback to alert if Materialize is not available + alert(message); + } +} + function applyI18n() { document.querySelectorAll('[data-i18n]').forEach((el) => { const key = el.getAttribute('data-i18n'); @@ -717,6 +745,24 @@ document.addEventListener('DOMContentLoaded', () => { } generateBtn.addEventListener('click', () => { + // Validate date range before generating report + const startDateInput = document.getElementById('startingDate'); + const endDateInput = document.getElementById('endingDate'); + const yesterdayRadio = document.getElementById('yesterdayContribution'); + + // Only validate if using custom date range (not yesterday preset) + if (!yesterdayRadio.checked && startDateInput && endDateInput) { + const startDate = startDateInput.value; + const endDate = endDateInput.value; + + if (!validateDateRange(startDate, endDate)) { + const errorMessage = chrome?.i18n.getMessage('invalidDateRangeError') || + 'Invalid date range. Please select a valid date range.'; + showDateValidationError(errorMessage); + return; // Stop report generation + } + } + browser.storage.local.get(['platform']).then((result) => { const platform = result.platform || 'github'; const platformUsernameKey = `${platform}Username`; @@ -1000,6 +1046,36 @@ document.addEventListener('DOMContentLoaded', () => { browser.storage.local.set({ yesterdayContribution: yesterdayRadio.checked }); }); startingDateInput.addEventListener('input', () => { + browser.storage.local.set({ startingDate: startingDateInput.value }); + + // Real-time validation for custom date ranges + const yesterdayRadio = document.getElementById('yesterdayContribution'); + if (!yesterdayRadio.checked) { + const startDate = startingDateInput.value; + const endDate = endingDateInput.value; + + if (startDate && endDate && !validateDateRange(startDate, endDate)) { + const errorMessage = chrome?.i18n.getMessage('invalidDateRangeError') || + 'Invalid date range. Please select a valid date range.'; + showDateValidationError(errorMessage); + } + } + }); + endingDateInput.addEventListener('input', () => { + browser.storage.local.set({ endingDate: endingDateInput.value }); + + // Real-time validation for custom date ranges + const yesterdayRadio = document.getElementById('yesterdayContribution'); + if (!yesterdayRadio.checked) { + const startDate = startingDateInput.value; + const endDate = endingDateInput.value; + + if (startDate && endDate && !validateDateRange(startDate, endDate)) { + const errorMessage = chrome?.i18n.getMessage('invalidDateRangeError') || + 'Invalid date range. Please select a valid date range.'; + showDateValidationError(errorMessage); + } + } window.scrumDateRangeUtils.normalizeSyncAndPersistDateRange( startingDateInput, endingDateInput, diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index c5fedf2b..012d274b 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -63,6 +63,7 @@ function allIncluded(outputTarget = 'email') { let nextWeekArray = []; let reviewedPrsArray = []; let githubIssuesData = null; + let githubCommitsData = null; let yesterdayContribution = false; let githubPrsReviewData = null; let githubUserData = null; @@ -278,12 +279,24 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; githubUserData = mappedData.githubUserData; + githubCommitsData = mappedCommits; const name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; @@ -348,9 +361,20 @@ function allIncluded(outputTarget = 'email') { const mappedMRs = (data.mergeRequests || data.mrs || []).map((mr) => mapGitLabItem(mr, data.projects, 'mr'), ); + // Map commits to standard format for report generation + const mappedCommits = (data.commits || []).map((commit) => ({ + ...commit, + message: commit.message || commit.title, + html_url: commit.web_url || (commit.project_url ? `${commit.project_url}/-/commit/${commit.id}` : ''), + sha: commit.id, + project: commit.project_name, + author_name: commit.author_name, + author_email: commit.author_email, + })); const mappedData = { githubIssuesData: { items: mappedIssues }, githubPrsReviewData: { items: mappedMRs }, + gitlabCommits: mappedCommits, githubUserData: data.user || {}, }; processGithubData(mappedData); @@ -1072,6 +1096,10 @@ function allIncluded(outputTarget = 'email') { } else if (platform === 'gitlab') { await writeGithubIssuesPrs(githubIssuesData?.items || []); await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + // Add commits to the report for GitLab + if (githubCommitsData && githubCommitsData.length > 0) { + await writeGithubIssuesPrs(githubCommitsData); + } } await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body');