From d6190c4b375884bff4b28a6872813febffae6718 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 16 Jun 2025 12:44:50 +0530 Subject: [PATCH 01/40] added support for gitlab --- src/manifest.json | 37 ++-- src/popup.html | 177 +++++++++------- src/scripts/main.js | 80 ++++++-- src/scripts/scrumHelper.js | 403 ++++++++++++++++++++++++++++++------- 4 files changed, 515 insertions(+), 182 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index 8a427ad..a04cb77 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -17,27 +17,34 @@ "content_scripts": [ { "matches": [ - "*://groups.google.com/forum/*", + "*://groups.google.com/forum/*", "*://groups.google.com/g/*", - "*://mail.google.com/*", - "*://outlook.live.com/*", - "*://outlook.office.com/*", - "*://mail.yahoo.com/*" + "*://mail.google.com/*", + "*://outlook.live.com/*", + "*://outlook.office.com/*", + "*://mail.yahoo.com/*" ], - "js": ["scripts/jquery-3.2.1.min.js", "scripts/emailClientAdapter.js", "scripts/scrumHelper.js"] + "js": [ + "scripts/jquery-3.2.1.min.js", + "scripts/emailClientAdapter.js", + "scripts/scrumHelper.js" + ] } ], - "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" }, - "web_accessible_resources": [{ - "resources": [ - "/icons/night-mode.png", - "icons/night-mode.png" - ], - "matches": [""] - }], + "web_accessible_resources": [ + { + "resources": [ + "/icons/night-mode.png", + "icons/night-mode.png" + ], + "matches": [ + "" + ] + } + ], "permissions": [ "tabs", "storage", @@ -52,4 +59,4 @@ "*://*.yahoo.com/*", "https://api.github.com/*" ] -} +} \ No newline at end of file diff --git a/src/popup.html b/src/popup.html index 5df594b..513318b 100644 --- a/src/popup.html +++ b/src/popup.html @@ -1,5 +1,6 @@ + @@ -8,61 +9,90 @@ + html, + body { + width: 375px !important; + height: 600px !important; + max-height: 600px !important; + overflow-y: scroll; + background: #eae4e4; + } + +
-
-

Scrum Helper

- Night Mode -
-
-

Report your development progress by auto-fetching your Git activity for a selected period

-
- +
+

Scrum Helper

+ Night Mode +
+
+

Report your development progress by auto-fetching your Git activity for a selected period

+
+
- +
- +

Your Project Name

- +
-
+
+

Platform

+
+ + +
+
+

Your Github Username

- + +
+ - +

Fetch your contributions between:

- +
- +
- +
@@ -74,79 +104,84 @@

Your Github Username

- +

- +

What is blocking you from making progress?

- +
Scrum Report
-
+
- - -
+
+ -

Note:

    -
  • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. (See this issue.) +
  • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days + ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. + (See this issue.)
  • -
  • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually reviewing and editing the report to ensure accuracy before sharing +
  • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually + reviewing and editing the report to ensure accuracy before sharing
+
-
-
- -
-

- Made with ❤️ by FOSSASIA • - v2.0 -

+
+ +
+

+ Made with ❤️ by FOSSASIA • + v2.0 +

+
+
-
-
- + + \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js index 94fef36..f805454 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,5 +1,6 @@ var enableToggleElement = document.getElementById('enable'); var githubUsernameElement = document.getElementById('githubUsername'); +var gitlabUsernameElement = document.getElementById('gitlabUsername'); var projectNameElement = document.getElementById('projectName'); var lastWeekContributionElement = document.getElementById('lastWeekContribution'); let yesterdayContributionElement = document.getElementById('yesterdayContribution'); @@ -7,10 +8,15 @@ var startingDateElement = document.getElementById('startingDate'); var endingDateElement = document.getElementById('endingDate'); var showOpenLabelElement = document.getElementById('showOpenLabel'); var userReasonElement = document.getElementById('userReason'); +var platformRadios = document.getElementsByName('platform'); +var githubUsernameContainer = document.getElementById('githubUsernameContainer'); +var gitlabUsernameContainer = document.getElementById('gitlabUsernameContainer'); + function handleBodyOnLoad() { chrome.storage.local.get( [ 'githubUsername', + 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -20,11 +26,19 @@ function handleBodyOnLoad() { 'userReason', 'lastWeekContribution', 'yesterdayContribution', + 'platform', ], (items) => { if (items.githubUsername) { githubUsernameElement.value = items.githubUsername; } + if (items.gitlabUsername) { + gitlabUsernameElement.value = items.gitlabUsername; + } + if (items.platform) { + document.querySelector(`input[name="platform"][value="${items.platform}"]`).checked = true; + handlePlatformChange(items.platform); + } if (items.projectName) { projectNameElement.value = items.projectName; } @@ -55,7 +69,7 @@ function handleBodyOnLoad() { lastWeekContributionElement.checked = items.lastWeekContribution; handleLastWeekContributionChange(); } - else if (items.lastWeekContribution !== false) { + else if (items.lastWeekContribution !== false) { lastWeekContributionElement.checked = true; handleLastWeekContributionChange(); } @@ -63,7 +77,7 @@ function handleBodyOnLoad() { yesterdayContributionElement.checked = items.yesterdayContribution; handleYesterdayContributionChange(); } - else if (items.yesterdayContribution !== false) { + else if (items.yesterdayContribution !== false) { yesterdayContributionElement.checked = true; handleYesterdayContributionChange(); } @@ -87,21 +101,21 @@ function handleLastWeekContributionChange() { var labelElement = document.querySelector("label[for='lastWeekContribution']"); if (value) { - startingDateElement.disabled = true; - endingDateElement.disabled = true; - endingDateElement.value = getToday(); - startingDateElement.value = getLastWeek(); - handleEndingDateChange(); - handleStartingDateChange(); - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + startingDateElement.disabled = true; + endingDateElement.disabled = true; + endingDateElement.value = getToday(); + startingDateElement.value = getLastWeek(); + handleEndingDateChange(); + handleStartingDateChange(); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - startingDateElement.disabled = false; - endingDateElement.disabled = false; - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + startingDateElement.disabled = false; + endingDateElement.disabled = false; + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } - + chrome.storage.local.set({ lastWeekContribution: value }); } @@ -114,8 +128,8 @@ function handleYesterdayContributionChange() { endingDateElement.disabled = true; endingDateElement.value = getToday(); startingDateElement.value = getYesterday(); - handleEndingDateChange(); - handleStartingDateChange(); + handleEndingDateChange(); + handleStartingDateChange(); labelElement.classList.add("selectedLabel"); labelElement.classList.remove("unselectedLabel"); } else { @@ -147,7 +161,7 @@ function getYesterday() { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayWeekDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -183,11 +197,11 @@ function handleOpenLabelChange() { var labelElement = document.querySelector("label[for='showOpenLabel']"); if (value) { - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } chrome.storage.local.set({ showOpenLabel: value }); @@ -197,6 +211,24 @@ function handleUserReasonChange() { var value = userReasonElement.value; chrome.storage.local.set({ userReason: value }); } + +function handlePlatformChange(platform) { + chrome.storage.local.set({ platform: platform }); + + if (platform === 'github') { + githubUsernameContainer.classList.remove('hidden'); + gitlabUsernameContainer.classList.add('hidden'); + } else { + githubUsernameContainer.classList.add('hidden'); + gitlabUsernameContainer.classList.remove('hidden'); + } +} + +function handleGitlabUsernameChange() { + var value = gitlabUsernameElement.value; + chrome.storage.local.set({ gitlabUsername: value }); +} + enableToggleElement.addEventListener('change', handleEnableChange); githubUsernameElement.addEventListener('keyup', handleGithubUsernameChange); projectNameElement.addEventListener('keyup', handleProjectNameChange); @@ -206,4 +238,8 @@ lastWeekContributionElement.addEventListener('change', handleLastWeekContributio yesterdayContributionElement.addEventListener('change', handleYesterdayContributionChange); showOpenLabelElement.addEventListener('change', handleOpenLabelChange); userReasonElement.addEventListener('keyup', handleUserReasonChange); +platformRadios.forEach(radio => { + radio.addEventListener('change', (e) => handlePlatformChange(e.target.value)); +}); +gitlabUsernameElement.addEventListener('keyup', handleGitlabUsernameChange); document.addEventListener('DOMContentLoaded', handleBodyOnLoad); \ No newline at end of file diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index b9f1e13..2de8acc 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1,24 +1,31 @@ console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); var enableToggle = true; +var gitlabProjectIdToName = {}; function allIncluded(outputTarget = 'email') { console.log('allIncluded called with outputTarget:', outputTarget); - console.log('Current window context:', window.location.href); + console.log('Current window context:', window.location.href); /* global $*/ var scrumBody = null; var scrumSubject = null; var startingDate = ''; var endingDate = ''; var githubUsername = ''; + var gitlabUsername = ''; var projectName = ''; + var platform = 'github'; var lastWeekArray = []; var nextWeekArray = []; var reviewedPrsArray = []; var githubIssuesData = null; + var gitlabIssuesData = null; var lastWeekContribution = false; let yesterdayContribution = false; var githubPrsReviewData = null; + var gitlabPrsReviewData = null; var githubUserData = null; + var gitlabUserData = null; var githubPrsReviewDataProcessed = {}; + var gitlabPrsReviewDataProcessed = {}; var showOpenLabel = true; var showClosedLabel = true; var userReason = ''; @@ -39,6 +46,7 @@ function allIncluded(outputTarget = 'email') { chrome.storage.local.get( [ 'githubUsername', + 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -48,10 +56,15 @@ function allIncluded(outputTarget = 'email') { 'lastWeekContribution', 'yesterdayContribution', 'userReason', + 'platform', ], (items) => { console.log("Storage items received:", items); - + + if (items.platform) { + platform = items.platform; + } + if (items.lastWeekContribution) { lastWeekContribution = true; handleLastWeekContributionChange(); @@ -69,31 +82,53 @@ function allIncluded(outputTarget = 'email') { if (items.startingDate && !lastWeekContribution) { startingDate = items.startingDate; } - if (items.endingDate && !yesterdayContribution){ + if (items.endingDate && !yesterdayContribution) { endingDate = items.endingDate; } - if (items.startingDate && !yesterdayContribution){ + if (items.startingDate && !yesterdayContribution) { startingDate = items.startingDate; } - if (items.githubUsername) { - githubUsername = items.githubUsername; - console.log("About to fetch GitHub data for:", githubUsername); - fetchGithubData(); - } else { - if (outputTarget === 'popup') { - console.log("No username found - popup context"); - // Show error in popup - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - Materialize.toast('Please enter your GitHub username', 3000); - } else { - console.log("No username found - email context"); - console.warn('No GitHub username found in storage'); - } - } + + if (platform === 'github') { + if (items.githubUsername) { + githubUsername = items.githubUsername; + console.log("About to fetch GitHub data for:", githubUsername); + fetchGithubData(); + } else { + if (outputTarget === 'popup') { + console.log("No GitHub username found - popup context"); + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + Materialize.toast('Please enter your GitHub username', 3000); + } else { + console.log("No GitHub username found - email context"); + console.warn('No GitHub username found in storage'); + } + } + } else { + if (items.gitlabUsername) { + gitlabUsername = items.gitlabUsername; + console.log("About to fetch GitLab data for:", gitlabUsername); + fetchGitlabData(); + } else { + if (outputTarget === 'popup') { + console.log("No GitLab username found - popup context"); + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + Materialize.toast('Please enter your GitLab username', 3000); + } else { + console.log("No GitLab username found - email context"); + console.warn('No GitLab username found in storage'); + } + } + } + if (items.projectName) { projectName = items.projectName; } @@ -147,7 +182,7 @@ function allIncluded(outputTarget = 'email') { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayWeekDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -246,12 +281,12 @@ function allIncluded(outputTarget = 'email') { function writeScrumBody() { if (!enableToggle) return; - if(outputTarget ==='email') { - if(!window.emailClientAdapter) { + if (outputTarget === 'email') { + if (!window.emailClientAdapter) { console.error('Email client adapter not found'); return; } - if(!window.emailClientAdapter.isNewConversation()) { + if (!window.emailClientAdapter.isNewConversation()) { console.log('Not a new conversation, skipping scrum helper'); return; } @@ -270,32 +305,32 @@ function allIncluded(outputTarget = 'email') { nextWeekUl += ''; var weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); - var weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + var weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; // Create the complete content let content; - if (lastWeekContribution == true || yesterdayContribution == true ) { - content = `1. What did I do ${weekOrDay}?
+ if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
${lastWeekUl}
2. What do I plan to do ${weekOrDay2}?
${nextWeekUl}
3. What is blocking me from making progress?
${userReason}`; - } else { - content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
+ } else { + content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
${lastWeekUl}
2. What do I plan to do ${weekOrDay2}?
${nextWeekUl}
3. What is blocking me from making progress?
${userReason}`; - } + } if (outputTarget === 'popup') { const scrumReport = document.getElementById('scrumReport'); if (scrumReport) { console.log("found div, updating content"); scrumReport.innerHTML = content; - + // Reset generate button const generateBtn = document.getElementById('generateReport'); if (generateBtn) { @@ -326,29 +361,29 @@ ${userReason}`; return project; } - function scrumSubjectLoaded() { - try{ + function scrumSubjectLoaded() { + try { - - if (!enableToggle) return; - if (!scrumSubject){ - console.error('Subject element not found'); - return; - } - setTimeout(() => { - var name = githubUserData.name || githubUsername; - var project = getProject(); - var curDate = new Date(); - var year = curDate.getFullYear().toString(); - var date = curDate.getDate(); - var month = curDate.getMonth(); - month++; - if (month < 10) month = '0' + month; - if (date < 10) date = '0' + date; - var dateCode = year.toString() + month.toString() + date.toString(); - scrumSubject.value = '[Scrum] ' + name + ' - ' + project + ' - ' + dateCode + ' - False'; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - }); + + if (!enableToggle) return; + if (!scrumSubject) { + console.error('Subject element not found'); + return; + } + setTimeout(() => { + var name = githubUserData.name || githubUsername; + var project = getProject(); + var curDate = new Date(); + var year = curDate.getFullYear().toString(); + var date = curDate.getDate(); + var month = curDate.getMonth(); + month++; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + var dateCode = year.toString() + month.toString() + date.toString(); + scrumSubject.value = '[Scrum] ' + name + ' - ' + project + ' - ' + dateCode + ' - False'; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + }); } catch (err) { console.err('Error while setting subject: ', err); } @@ -356,10 +391,10 @@ ${userReason}`; function writeGithubPrsReviews() { var items = githubPrsReviewData.items; - + reviewedPrsArray = []; githubPrsReviewDataProcessed = {}; - + for (var i = 0; i < items.length; i++) { var item = items[i]; console.log(`Review item ${i + 1}/${items.length}:`, { @@ -369,21 +404,21 @@ ${userReason}`; state: item.state, title: item.title }); - + if (item.user.login === githubUsername) { continue; } - + var repository_url = item.repository_url; var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); var title = item.title; var number = item.number; var html_url = item.html_url; - + if (!githubPrsReviewDataProcessed[project]) { githubPrsReviewDataProcessed[project] = []; } - + var obj = { number: number, html_url: html_url, @@ -392,7 +427,7 @@ ${userReason}`; }; githubPrsReviewDataProcessed[project].push(obj); } - + for (var repo in githubPrsReviewDataProcessed) { var repoLi = '
  • (' + repo + ') - Reviewed '; if (githubPrsReviewDataProcessed[repo].length > 1) { @@ -432,25 +467,25 @@ ${userReason}`; repoLi += '
  • '; reviewedPrsArray.push(repoLi); } - - writeScrumBody(); + + writeScrumBody(); } function writeGithubIssuesPrs() { var data = githubIssuesData; var items = data.items; - + lastWeekArray = []; nextWeekArray = []; - + for (var i = 0; i < items.length; i++) { - var item = items[i]; + var item = items[i]; var html_url = item.html_url; var repository_url = item.repository_url; var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); var title = item.title; var number = item.number; var li = ''; - + if (item.pull_request) { if (item.state === 'closed') { li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_merged_button}
  • `; @@ -500,7 +535,7 @@ ${userReason}`; clearInterval(intervalSubject); scrumSubject = elements.subject; - + setTimeout(() => { scrumSubjectLoaded(); }, 500); @@ -514,12 +549,232 @@ ${userReason}`; writeGithubPrsReviews(); } }, 500); + + // Helper functions for ISO date range + function getStartOfDayISO(dateString) { + return dateString + 'T00:00:00Z'; + } + function getEndOfDayISO(dateString) { + return dateString + 'T23:59:59Z'; + } + + function fetchGitlabData() { + // First get user's projects + var projectsUrl = `https://gitlab.com/api/v4/users/${gitlabUsername}/projects?per_page=100`; + console.log('[GitLab] Fetching projects for user:', gitlabUsername, 'URL:', projectsUrl); + + $.ajax({ + dataType: 'json', + type: 'GET', + url: projectsUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab projects:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + Materialize.toast('Error fetching GitLab projects. Please check your username.', 3000); + }, + success: (projects) => { + console.log('[GitLab] Projects fetched:', projects.map(p => ({ id: p.id, name: p.name, path_with_namespace: p.path_with_namespace }))); + if (!projects || projects.length === 0) { + Materialize.toast('No GitLab projects found for this user', 3000); + return; + } + + // After fetching projects: + projects.forEach(p => { + gitlabProjectIdToName[p.id] = p.name; + }); + + // Fetch issues and MRs for each project + var projectIds = projects.map(p => p.id); + var issuesPromises = []; + var mrsPromises = []; + + projectIds.forEach(projectId => { + // Fetch issues + var issuesUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching issues for project ${projectId}:`, issuesUrl); + issuesPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: issuesUrl, + success: function (data) { + console.log(`[GitLab][DEBUG] Raw issues response for project ${projectId}:`, data); + return data; + }, + error: function (xhr, textStatus, errorThrown) { + console.error(`[GitLab][ERROR] Issues API error for project ${projectId}:`, xhr.status, textStatus, errorThrown, xhr.responseText); + return []; + } + }) + ); + + // Fetch merge requests + var mrsUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching MRs for project ${projectId}:`, mrsUrl); + mrsPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: mrsUrl + }) + ); + }); + + // Process all issues + Promise.all(issuesPromises) + .then(results => { + gitlabIssuesData = results.flat(); + console.log('[GitLab] Issues fetched (after flatten):', gitlabIssuesData); + writeGitlabIssuesPrs(); + }) + .catch(error => { + console.error('Error fetching GitLab issues:', error); + Materialize.toast('Error fetching GitLab issues', 3000); + }); + + // Process all merge requests + Promise.all(mrsPromises) + .then(results => { + gitlabPrsReviewData = results.flat(); + console.log('[GitLab] Merge Requests fetched:', gitlabPrsReviewData.map(mr => ({ id: mr.id, title: mr.title, author: mr.author ? mr.author.username : undefined, project: mr.project ? mr.project.name : undefined }))); + writeGitlabPrsReviews(); + }) + .catch(error => { + console.error('Error fetching GitLab merge requests:', error); + Materialize.toast('Error fetching GitLab merge requests', 3000); + }); + } + }); + + // Fetch GitLab user data + var userUrl = `https://gitlab.com/api/v4/users?username=${gitlabUsername}`; + console.log('[GitLab] Fetching user data:', userUrl); + $.ajax({ + dataType: 'json', + type: 'GET', + url: userUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab user data:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + }, + success: (data) => { + if (data && data.length > 0) { + gitlabUserData = data[0]; + console.log('[GitLab] User data:', gitlabUserData); + } + }, + }); + } + + function writeGitlabIssuesPrs() { + if (!gitlabIssuesData) return; + + lastWeekArray = []; + nextWeekArray = []; + + for (var i = 0; i < gitlabIssuesData.length; i++) { + var item = gitlabIssuesData[i]; + var web_url = item.web_url; + var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var title = item.title; + var iid = item.iid; + var li = ''; + + if (item.state === 'closed') { + li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_closed_button}
  • `; + } else if (item.state === 'opened') { + li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_opened_button}
  • `; + } + + if (li) { + lastWeekArray.push(li); + } + } + console.log('[GitLab] Scrum lastWeekArray (issues):', lastWeekArray); + writeScrumBody(); + } + + function writeGitlabPrsReviews() { + if (!gitlabPrsReviewData) return; + + reviewedPrsArray = []; + gitlabPrsReviewDataProcessed = {}; + + for (var i = 0; i < gitlabPrsReviewData.length; i++) { + var item = gitlabPrsReviewData[i]; + var web_url = item.web_url; + var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var title = item.title; + var iid = item.iid; + + if (!gitlabPrsReviewDataProcessed[project]) { + gitlabPrsReviewDataProcessed[project] = []; + } + + var obj = { + number: iid, + html_url: web_url, + title: title, + state: item.state, + }; + gitlabPrsReviewDataProcessed[project].push(obj); + } + + for (var repo in gitlabPrsReviewDataProcessed) { + var repoLi = '
  • (' + repo + ') - Created '; + if (gitlabPrsReviewDataProcessed[repo].length > 1) { + repoLi += 'MRs - '; + } else { + repoLi += 'MR - '; + } + if (gitlabPrsReviewDataProcessed[repo].length <= 1) { + for (var pr in gitlabPrsReviewDataProcessed[repo]) { + var pr_arr = gitlabPrsReviewDataProcessed[repo][pr]; + var prText = ''; + prText += `#${pr_arr.number} (${pr_arr.title}) `; + if (pr_arr.state === 'opened') { + prText += issue_opened_button; + } else { + prText += issue_closed_button; + } + prText += '  '; + repoLi += prText; + } + } else { + repoLi += '
      '; + for (var pr1 in gitlabPrsReviewDataProcessed[repo]) { + var pr_arr1 = gitlabPrsReviewDataProcessed[repo][pr1]; + var prText1 = ''; + prText1 += `
    • #${pr_arr1.number} (${pr_arr1.title}) `; + if (pr_arr1.state === 'opened') { + prText1 += issue_opened_button; + } else { + prText1 += issue_closed_button; + } + prText1 += '  
    • '; + repoLi += prText1; + } + repoLi += '
    '; + } + repoLi += '
  • '; + reviewedPrsArray.push(repoLi); + } + console.log('[GitLab] Scrum reviewedPrsArray (MRs):', reviewedPrsArray); + writeScrumBody(); + } } -allIncluded('email'); +allIncluded('email'); $('button>span:contains(New conversation)').parent('button').click(() => { - allIncluded(); + allIncluded(); }); -window.generateScrumReport = function() { - allIncluded('popup'); +window.generateScrumReport = function () { + allIncluded('popup'); }; \ No newline at end of file From ae0420145e898a0a6cfd97e784d296522cbfa10c Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 18 Jun 2025 16:23:03 +0530 Subject: [PATCH 02/40] added support for gitlab --- src/manifest.json | 37 ++-- src/popup.html | 177 +++++++++------- src/scripts/main.js | 80 +++++-- src/scripts/scrumHelper.js | 420 ++++++++++++++++++++++++++++++------- 4 files changed, 532 insertions(+), 182 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index 8a427ad..a04cb77 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -17,27 +17,34 @@ "content_scripts": [ { "matches": [ - "*://groups.google.com/forum/*", + "*://groups.google.com/forum/*", "*://groups.google.com/g/*", - "*://mail.google.com/*", - "*://outlook.live.com/*", - "*://outlook.office.com/*", - "*://mail.yahoo.com/*" + "*://mail.google.com/*", + "*://outlook.live.com/*", + "*://outlook.office.com/*", + "*://mail.yahoo.com/*" ], - "js": ["scripts/jquery-3.2.1.min.js", "scripts/emailClientAdapter.js", "scripts/scrumHelper.js"] + "js": [ + "scripts/jquery-3.2.1.min.js", + "scripts/emailClientAdapter.js", + "scripts/scrumHelper.js" + ] } ], - "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" }, - "web_accessible_resources": [{ - "resources": [ - "/icons/night-mode.png", - "icons/night-mode.png" - ], - "matches": [""] - }], + "web_accessible_resources": [ + { + "resources": [ + "/icons/night-mode.png", + "icons/night-mode.png" + ], + "matches": [ + "" + ] + } + ], "permissions": [ "tabs", "storage", @@ -52,4 +59,4 @@ "*://*.yahoo.com/*", "https://api.github.com/*" ] -} +} \ No newline at end of file diff --git a/src/popup.html b/src/popup.html index 5df594b..513318b 100644 --- a/src/popup.html +++ b/src/popup.html @@ -1,5 +1,6 @@ + @@ -8,61 +9,90 @@ + html, + body { + width: 375px !important; + height: 600px !important; + max-height: 600px !important; + overflow-y: scroll; + background: #eae4e4; + } + +
    -
    -

    Scrum Helper

    - Night Mode -
    -
    -

    Report your development progress by auto-fetching your Git activity for a selected period

    -
    - +
    +

    Scrum Helper

    + Night Mode +
    +
    +

    Report your development progress by auto-fetching your Git activity for a selected period

    +
    +
    - +
    - +

    Your Project Name

    - +
    -
    +
    +

    Platform

    +
    + + +
    +
    +

    Your Github Username

    - + +
    + - +

    Fetch your contributions between:

    - +
    - +
    - +
    @@ -74,79 +104,84 @@

    Your Github Username

    - +

    - +

    What is blocking you from making progress?

    - +
    Scrum Report
    -
    +
    - - -
    +
    + -

    Note:

      -
    • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. (See this issue.) +
    • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days + ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. + (See this issue.)
    • -
    • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually reviewing and editing the report to ensure accuracy before sharing +
    • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually + reviewing and editing the report to ensure accuracy before sharing
    +
    -
    -
    - -
    -

    - Made with ❤️ by FOSSASIA • - v2.0 -

    +
    + +
    +

    + Made with ❤️ by FOSSASIA • + v2.0 +

    +
    +
    -
    -
    - + + \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js index 94fef36..f805454 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,5 +1,6 @@ var enableToggleElement = document.getElementById('enable'); var githubUsernameElement = document.getElementById('githubUsername'); +var gitlabUsernameElement = document.getElementById('gitlabUsername'); var projectNameElement = document.getElementById('projectName'); var lastWeekContributionElement = document.getElementById('lastWeekContribution'); let yesterdayContributionElement = document.getElementById('yesterdayContribution'); @@ -7,10 +8,15 @@ var startingDateElement = document.getElementById('startingDate'); var endingDateElement = document.getElementById('endingDate'); var showOpenLabelElement = document.getElementById('showOpenLabel'); var userReasonElement = document.getElementById('userReason'); +var platformRadios = document.getElementsByName('platform'); +var githubUsernameContainer = document.getElementById('githubUsernameContainer'); +var gitlabUsernameContainer = document.getElementById('gitlabUsernameContainer'); + function handleBodyOnLoad() { chrome.storage.local.get( [ 'githubUsername', + 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -20,11 +26,19 @@ function handleBodyOnLoad() { 'userReason', 'lastWeekContribution', 'yesterdayContribution', + 'platform', ], (items) => { if (items.githubUsername) { githubUsernameElement.value = items.githubUsername; } + if (items.gitlabUsername) { + gitlabUsernameElement.value = items.gitlabUsername; + } + if (items.platform) { + document.querySelector(`input[name="platform"][value="${items.platform}"]`).checked = true; + handlePlatformChange(items.platform); + } if (items.projectName) { projectNameElement.value = items.projectName; } @@ -55,7 +69,7 @@ function handleBodyOnLoad() { lastWeekContributionElement.checked = items.lastWeekContribution; handleLastWeekContributionChange(); } - else if (items.lastWeekContribution !== false) { + else if (items.lastWeekContribution !== false) { lastWeekContributionElement.checked = true; handleLastWeekContributionChange(); } @@ -63,7 +77,7 @@ function handleBodyOnLoad() { yesterdayContributionElement.checked = items.yesterdayContribution; handleYesterdayContributionChange(); } - else if (items.yesterdayContribution !== false) { + else if (items.yesterdayContribution !== false) { yesterdayContributionElement.checked = true; handleYesterdayContributionChange(); } @@ -87,21 +101,21 @@ function handleLastWeekContributionChange() { var labelElement = document.querySelector("label[for='lastWeekContribution']"); if (value) { - startingDateElement.disabled = true; - endingDateElement.disabled = true; - endingDateElement.value = getToday(); - startingDateElement.value = getLastWeek(); - handleEndingDateChange(); - handleStartingDateChange(); - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + startingDateElement.disabled = true; + endingDateElement.disabled = true; + endingDateElement.value = getToday(); + startingDateElement.value = getLastWeek(); + handleEndingDateChange(); + handleStartingDateChange(); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - startingDateElement.disabled = false; - endingDateElement.disabled = false; - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + startingDateElement.disabled = false; + endingDateElement.disabled = false; + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } - + chrome.storage.local.set({ lastWeekContribution: value }); } @@ -114,8 +128,8 @@ function handleYesterdayContributionChange() { endingDateElement.disabled = true; endingDateElement.value = getToday(); startingDateElement.value = getYesterday(); - handleEndingDateChange(); - handleStartingDateChange(); + handleEndingDateChange(); + handleStartingDateChange(); labelElement.classList.add("selectedLabel"); labelElement.classList.remove("unselectedLabel"); } else { @@ -147,7 +161,7 @@ function getYesterday() { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayWeekDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -183,11 +197,11 @@ function handleOpenLabelChange() { var labelElement = document.querySelector("label[for='showOpenLabel']"); if (value) { - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } chrome.storage.local.set({ showOpenLabel: value }); @@ -197,6 +211,24 @@ function handleUserReasonChange() { var value = userReasonElement.value; chrome.storage.local.set({ userReason: value }); } + +function handlePlatformChange(platform) { + chrome.storage.local.set({ platform: platform }); + + if (platform === 'github') { + githubUsernameContainer.classList.remove('hidden'); + gitlabUsernameContainer.classList.add('hidden'); + } else { + githubUsernameContainer.classList.add('hidden'); + gitlabUsernameContainer.classList.remove('hidden'); + } +} + +function handleGitlabUsernameChange() { + var value = gitlabUsernameElement.value; + chrome.storage.local.set({ gitlabUsername: value }); +} + enableToggleElement.addEventListener('change', handleEnableChange); githubUsernameElement.addEventListener('keyup', handleGithubUsernameChange); projectNameElement.addEventListener('keyup', handleProjectNameChange); @@ -206,4 +238,8 @@ lastWeekContributionElement.addEventListener('change', handleLastWeekContributio yesterdayContributionElement.addEventListener('change', handleYesterdayContributionChange); showOpenLabelElement.addEventListener('change', handleOpenLabelChange); userReasonElement.addEventListener('keyup', handleUserReasonChange); +platformRadios.forEach(radio => { + radio.addEventListener('change', (e) => handlePlatformChange(e.target.value)); +}); +gitlabUsernameElement.addEventListener('keyup', handleGitlabUsernameChange); document.addEventListener('DOMContentLoaded', handleBodyOnLoad); \ No newline at end of file diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index b9f1e13..96d0177 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1,24 +1,32 @@ console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); var enableToggle = true; +var gitlabProjectIdToName = {}; +var gitlabOrgFilter = ''; function allIncluded(outputTarget = 'email') { console.log('allIncluded called with outputTarget:', outputTarget); - console.log('Current window context:', window.location.href); + console.log('Current window context:', window.location.href); /* global $*/ var scrumBody = null; var scrumSubject = null; var startingDate = ''; var endingDate = ''; var githubUsername = ''; + var gitlabUsername = ''; var projectName = ''; + var platform = 'github'; var lastWeekArray = []; var nextWeekArray = []; var reviewedPrsArray = []; var githubIssuesData = null; + var gitlabIssuesData = null; var lastWeekContribution = false; let yesterdayContribution = false; var githubPrsReviewData = null; + var gitlabPrsReviewData = null; var githubUserData = null; + var gitlabUserData = null; var githubPrsReviewDataProcessed = {}; + var gitlabPrsReviewDataProcessed = {}; var showOpenLabel = true; var showClosedLabel = true; var userReason = ''; @@ -39,6 +47,7 @@ function allIncluded(outputTarget = 'email') { chrome.storage.local.get( [ 'githubUsername', + 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -48,10 +57,16 @@ function allIncluded(outputTarget = 'email') { 'lastWeekContribution', 'yesterdayContribution', 'userReason', + 'platform', + 'gitlabOrgFilter', ], (items) => { console.log("Storage items received:", items); - + + if (items.platform) { + platform = items.platform; + } + if (items.lastWeekContribution) { lastWeekContribution = true; handleLastWeekContributionChange(); @@ -69,31 +84,53 @@ function allIncluded(outputTarget = 'email') { if (items.startingDate && !lastWeekContribution) { startingDate = items.startingDate; } - if (items.endingDate && !yesterdayContribution){ + if (items.endingDate && !yesterdayContribution) { endingDate = items.endingDate; } - if (items.startingDate && !yesterdayContribution){ + if (items.startingDate && !yesterdayContribution) { startingDate = items.startingDate; } - if (items.githubUsername) { - githubUsername = items.githubUsername; - console.log("About to fetch GitHub data for:", githubUsername); - fetchGithubData(); - } else { - if (outputTarget === 'popup') { - console.log("No username found - popup context"); - // Show error in popup - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - Materialize.toast('Please enter your GitHub username', 3000); - } else { - console.log("No username found - email context"); - console.warn('No GitHub username found in storage'); - } - } + + if (platform === 'github') { + if (items.githubUsername) { + githubUsername = items.githubUsername; + console.log("About to fetch GitHub data for:", githubUsername); + fetchGithubData(); + } else { + if (outputTarget === 'popup') { + console.log("No GitHub username found - popup context"); + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + Materialize.toast('Please enter your GitHub username', 3000); + } else { + console.log("No GitHub username found - email context"); + console.warn('No GitHub username found in storage'); + } + } + } else { + if (items.gitlabUsername) { + gitlabUsername = items.gitlabUsername; + console.log("About to fetch GitLab data for:", gitlabUsername); + fetchGitlabData(); + } else { + if (outputTarget === 'popup') { + console.log("No GitLab username found - popup context"); + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + Materialize.toast('Please enter your GitLab username', 3000); + } else { + console.log("No GitLab username found - email context"); + console.warn('No GitLab username found in storage'); + } + } + } + if (items.projectName) { projectName = items.projectName; } @@ -114,6 +151,10 @@ function allIncluded(outputTarget = 'email') { if (!items.userReason) { userReason = 'No Blocker at the moment'; } + + if (items.gitlabOrgFilter) { + gitlabOrgFilter = items.gitlabOrgFilter; + } }, ); } @@ -147,7 +188,7 @@ function allIncluded(outputTarget = 'email') { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayWeekDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -246,12 +287,12 @@ function allIncluded(outputTarget = 'email') { function writeScrumBody() { if (!enableToggle) return; - if(outputTarget ==='email') { - if(!window.emailClientAdapter) { + if (outputTarget === 'email') { + if (!window.emailClientAdapter) { console.error('Email client adapter not found'); return; } - if(!window.emailClientAdapter.isNewConversation()) { + if (!window.emailClientAdapter.isNewConversation()) { console.log('Not a new conversation, skipping scrum helper'); return; } @@ -270,32 +311,32 @@ function allIncluded(outputTarget = 'email') { nextWeekUl += ''; var weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); - var weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + var weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; // Create the complete content let content; - if (lastWeekContribution == true || yesterdayContribution == true ) { - content = `1. What did I do ${weekOrDay}?
    + if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } else { - content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    + } else { + content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } + } if (outputTarget === 'popup') { const scrumReport = document.getElementById('scrumReport'); if (scrumReport) { console.log("found div, updating content"); scrumReport.innerHTML = content; - + // Reset generate button const generateBtn = document.getElementById('generateReport'); if (generateBtn) { @@ -326,29 +367,29 @@ ${userReason}`; return project; } - function scrumSubjectLoaded() { - try{ + function scrumSubjectLoaded() { + try { - - if (!enableToggle) return; - if (!scrumSubject){ - console.error('Subject element not found'); - return; - } - setTimeout(() => { - var name = githubUserData.name || githubUsername; - var project = getProject(); - var curDate = new Date(); - var year = curDate.getFullYear().toString(); - var date = curDate.getDate(); - var month = curDate.getMonth(); - month++; - if (month < 10) month = '0' + month; - if (date < 10) date = '0' + date; - var dateCode = year.toString() + month.toString() + date.toString(); - scrumSubject.value = '[Scrum] ' + name + ' - ' + project + ' - ' + dateCode + ' - False'; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - }); + + if (!enableToggle) return; + if (!scrumSubject) { + console.error('Subject element not found'); + return; + } + setTimeout(() => { + var name = githubUserData.name || githubUsername; + var project = getProject(); + var curDate = new Date(); + var year = curDate.getFullYear().toString(); + var date = curDate.getDate(); + var month = curDate.getMonth(); + month++; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + var dateCode = year.toString() + month.toString() + date.toString(); + scrumSubject.value = '[Scrum] ' + name + ' - ' + project + ' - ' + dateCode + ' - False'; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + }); } catch (err) { console.err('Error while setting subject: ', err); } @@ -356,10 +397,10 @@ ${userReason}`; function writeGithubPrsReviews() { var items = githubPrsReviewData.items; - + reviewedPrsArray = []; githubPrsReviewDataProcessed = {}; - + for (var i = 0; i < items.length; i++) { var item = items[i]; console.log(`Review item ${i + 1}/${items.length}:`, { @@ -369,21 +410,21 @@ ${userReason}`; state: item.state, title: item.title }); - + if (item.user.login === githubUsername) { continue; } - + var repository_url = item.repository_url; var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); var title = item.title; var number = item.number; var html_url = item.html_url; - + if (!githubPrsReviewDataProcessed[project]) { githubPrsReviewDataProcessed[project] = []; } - + var obj = { number: number, html_url: html_url, @@ -392,7 +433,7 @@ ${userReason}`; }; githubPrsReviewDataProcessed[project].push(obj); } - + for (var repo in githubPrsReviewDataProcessed) { var repoLi = '
  • (' + repo + ') - Reviewed '; if (githubPrsReviewDataProcessed[repo].length > 1) { @@ -432,25 +473,25 @@ ${userReason}`; repoLi += '
  • '; reviewedPrsArray.push(repoLi); } - - writeScrumBody(); + + writeScrumBody(); } function writeGithubIssuesPrs() { var data = githubIssuesData; var items = data.items; - + lastWeekArray = []; nextWeekArray = []; - + for (var i = 0; i < items.length; i++) { - var item = items[i]; + var item = items[i]; var html_url = item.html_url; var repository_url = item.repository_url; var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); var title = item.title; var number = item.number; var li = ''; - + if (item.pull_request) { if (item.state === 'closed') { li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_merged_button}
  • `; @@ -500,7 +541,7 @@ ${userReason}`; clearInterval(intervalSubject); scrumSubject = elements.subject; - + setTimeout(() => { scrumSubjectLoaded(); }, 500); @@ -514,12 +555,243 @@ ${userReason}`; writeGithubPrsReviews(); } }, 500); + + // Helper functions for ISO date range + function getStartOfDayISO(dateString) { + return dateString + 'T00:00:00Z'; + } + function getEndOfDayISO(dateString) { + return dateString + 'T23:59:59Z'; + } + + function fetchGitlabData() { + // Get the organization/group filter from storage (if any) + chrome.storage.local.get(['gitlabOrgFilter'], function (orgItems) { + gitlabOrgFilter = orgItems.gitlabOrgFilter || ''; + + var projectsUrl = `https://gitlab.com/api/v4/users/${gitlabUsername}/projects?per_page=100`; + console.log('[GitLab] Fetching projects for user:', gitlabUsername, 'URL:', projectsUrl); + + $.ajax({ + dataType: 'json', + type: 'GET', + url: projectsUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab projects:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + Materialize.toast('Error fetching GitLab projects. Please check your username.', 3000); + }, + success: (projects) => { + // Build projectId-to-name map + gitlabProjectIdToName = {}; + projects.forEach(p => { + gitlabProjectIdToName[p.id] = p.name; + }); + + // Filter projects by organization/group if filter is set + let filteredProjects = projects; + if (gitlabOrgFilter && gitlabOrgFilter.trim() !== '') { + filteredProjects = projects.filter(p => p.path_with_namespace.toLowerCase().startsWith(gitlabOrgFilter.toLowerCase() + '/')); + console.log('[GitLab] Filtering projects by org/group:', gitlabOrgFilter, 'Filtered:', filteredProjects.map(p => p.path_with_namespace)); + } + console.log('[GitLab] Projects fetched:', filteredProjects.map(p => ({ id: p.id, name: p.name, path_with_namespace: p.path_with_namespace }))); + if (!filteredProjects || filteredProjects.length === 0) { + Materialize.toast('No GitLab projects found for this user and organization/group', 3000); + return; + } + + // Fetch issues and MRs for each filtered project + var projectIds = filteredProjects.map(p => p.id); + var issuesPromises = []; + var mrsPromises = []; + + projectIds.forEach(projectId => { + // Fetch issues + var issuesUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching issues for project ${projectId}:`, issuesUrl); + issuesPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: issuesUrl, + success: function (data) { + console.log(`[GitLab][DEBUG] Raw issues response for project ${projectId}:`, data); + return data; + }, + error: function (xhr, textStatus, errorThrown) { + console.error(`[GitLab][ERROR] Issues API error for project ${projectId}:`, xhr.status, textStatus, errorThrown, xhr.responseText); + return []; + } + }) + ); + + // Fetch merge requests + var mrsUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching MRs for project ${projectId}:`, mrsUrl); + mrsPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: mrsUrl + }) + ); + }); + + // Process all issues + Promise.all(issuesPromises) + .then(results => { + gitlabIssuesData = results.flat(); + console.log('[GitLab] Issues fetched (after flatten):', gitlabIssuesData); + writeGitlabIssuesPrs(); + }) + .catch(error => { + console.error('Error fetching GitLab issues:', error); + Materialize.toast('Error fetching GitLab issues', 3000); + }); + + // Process all merge requests + Promise.all(mrsPromises) + .then(results => { + gitlabPrsReviewData = results.flat(); + console.log('[GitLab] Merge Requests fetched:', gitlabPrsReviewData.map(mr => ({ id: mr.id, title: mr.title, author: mr.author ? mr.author.username : undefined, project: mr.project_id ? gitlabProjectIdToName[mr.project_id] : undefined }))); + writeGitlabPrsReviews(); + }) + .catch(error => { + console.error('Error fetching GitLab merge requests:', error); + Materialize.toast('Error fetching GitLab merge requests', 3000); + }); + } + }); + + // Fetch GitLab user data + var userUrl = `https://gitlab.com/api/v4/users?username=${gitlabUsername}`; + console.log('[GitLab] Fetching user data:', userUrl); + $.ajax({ + dataType: 'json', + type: 'GET', + url: userUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab user data:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + }, + success: (data) => { + if (data && data.length > 0) { + gitlabUserData = data[0]; + console.log('[GitLab] User data:', gitlabUserData); + } + }, + }); + }); + } + + function writeGitlabIssuesPrs() { + if (!gitlabIssuesData) return; + + lastWeekArray = []; + nextWeekArray = []; + + for (var i = 0; i < gitlabIssuesData.length; i++) { + var item = gitlabIssuesData[i]; + var web_url = item.web_url; + var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var title = item.title; + var iid = item.iid; + var li = ''; + + if (item.state === 'closed') { + li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_closed_button}
  • `; + } else if (item.state === 'opened') { + li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_opened_button}
  • `; + } + + if (li) { + lastWeekArray.push(li); + } + } + console.log('[GitLab] Scrum lastWeekArray (issues):', lastWeekArray); + writeScrumBody(); + } + + function writeGitlabPrsReviews() { + if (!gitlabPrsReviewData) return; + + reviewedPrsArray = []; + gitlabPrsReviewDataProcessed = {}; + + for (var i = 0; i < gitlabPrsReviewData.length; i++) { + var item = gitlabPrsReviewData[i]; + var web_url = item.web_url; + var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var title = item.title; + var iid = item.iid; + + if (!gitlabPrsReviewDataProcessed[project]) { + gitlabPrsReviewDataProcessed[project] = []; + } + + var obj = { + number: iid, + html_url: web_url, + title: title, + state: item.state, + }; + gitlabPrsReviewDataProcessed[project].push(obj); + } + + for (var repo in gitlabPrsReviewDataProcessed) { + var repoLi = '
  • (' + repo + ') - Created '; + if (gitlabPrsReviewDataProcessed[repo].length > 1) { + repoLi += 'MRs - '; + } else { + repoLi += 'MR - '; + } + if (gitlabPrsReviewDataProcessed[repo].length <= 1) { + for (var pr in gitlabPrsReviewDataProcessed[repo]) { + var pr_arr = gitlabPrsReviewDataProcessed[repo][pr]; + var prText = ''; + prText += `#${pr_arr.number} (${pr_arr.title}) `; + if (pr_arr.state === 'opened') { + prText += issue_opened_button; + } else { + prText += issue_closed_button; + } + prText += '  '; + repoLi += prText; + } + } else { + repoLi += '
      '; + for (var pr1 in gitlabPrsReviewDataProcessed[repo]) { + var pr_arr1 = gitlabPrsReviewDataProcessed[repo][pr1]; + var prText1 = ''; + prText1 += `
    • #${pr_arr1.number} (${pr_arr1.title}) `; + if (pr_arr1.state === 'opened') { + prText1 += issue_opened_button; + } else { + prText1 += issue_closed_button; + } + prText1 += '  
    • '; + repoLi += prText1; + } + repoLi += '
    '; + } + repoLi += '
  • '; + reviewedPrsArray.push(repoLi); + } + console.log('[GitLab] Scrum reviewedPrsArray (MRs):', reviewedPrsArray); + writeScrumBody(); + } } -allIncluded('email'); +allIncluded('email'); $('button>span:contains(New conversation)').parent('button').click(() => { - allIncluded(); + allIncluded(); }); -window.generateScrumReport = function() { - allIncluded('popup'); +window.generateScrumReport = function () { + allIncluded('popup'); }; \ No newline at end of file From 8f2fc23fc8bbfe2461a64e1607df009808186cb5 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 18 Jun 2025 16:39:10 +0530 Subject: [PATCH 03/40] making same as pr --- src/scripts/scrumHelper.js | 225 +++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 121 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 96d0177..2de8acc 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1,7 +1,6 @@ console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); var enableToggle = true; var gitlabProjectIdToName = {}; -var gitlabOrgFilter = ''; function allIncluded(outputTarget = 'email') { console.log('allIncluded called with outputTarget:', outputTarget); console.log('Current window context:', window.location.href); @@ -58,7 +57,6 @@ function allIncluded(outputTarget = 'email') { 'yesterdayContribution', 'userReason', 'platform', - 'gitlabOrgFilter', ], (items) => { console.log("Storage items received:", items); @@ -151,10 +149,6 @@ function allIncluded(outputTarget = 'email') { if (!items.userReason) { userReason = 'No Blocker at the moment'; } - - if (items.gitlabOrgFilter) { - gitlabOrgFilter = items.gitlabOrgFilter; - } }, ); } @@ -565,128 +559,117 @@ ${userReason}`; } function fetchGitlabData() { - // Get the organization/group filter from storage (if any) - chrome.storage.local.get(['gitlabOrgFilter'], function (orgItems) { - gitlabOrgFilter = orgItems.gitlabOrgFilter || ''; - - var projectsUrl = `https://gitlab.com/api/v4/users/${gitlabUsername}/projects?per_page=100`; - console.log('[GitLab] Fetching projects for user:', gitlabUsername, 'URL:', projectsUrl); - - $.ajax({ - dataType: 'json', - type: 'GET', - url: projectsUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab projects:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - Materialize.toast('Error fetching GitLab projects. Please check your username.', 3000); - }, - success: (projects) => { - // Build projectId-to-name map - gitlabProjectIdToName = {}; - projects.forEach(p => { - gitlabProjectIdToName[p.id] = p.name; - }); + // First get user's projects + var projectsUrl = `https://gitlab.com/api/v4/users/${gitlabUsername}/projects?per_page=100`; + console.log('[GitLab] Fetching projects for user:', gitlabUsername, 'URL:', projectsUrl); - // Filter projects by organization/group if filter is set - let filteredProjects = projects; - if (gitlabOrgFilter && gitlabOrgFilter.trim() !== '') { - filteredProjects = projects.filter(p => p.path_with_namespace.toLowerCase().startsWith(gitlabOrgFilter.toLowerCase() + '/')); - console.log('[GitLab] Filtering projects by org/group:', gitlabOrgFilter, 'Filtered:', filteredProjects.map(p => p.path_with_namespace)); - } - console.log('[GitLab] Projects fetched:', filteredProjects.map(p => ({ id: p.id, name: p.name, path_with_namespace: p.path_with_namespace }))); - if (!filteredProjects || filteredProjects.length === 0) { - Materialize.toast('No GitLab projects found for this user and organization/group', 3000); - return; - } + $.ajax({ + dataType: 'json', + type: 'GET', + url: projectsUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab projects:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + Materialize.toast('Error fetching GitLab projects. Please check your username.', 3000); + }, + success: (projects) => { + console.log('[GitLab] Projects fetched:', projects.map(p => ({ id: p.id, name: p.name, path_with_namespace: p.path_with_namespace }))); + if (!projects || projects.length === 0) { + Materialize.toast('No GitLab projects found for this user', 3000); + return; + } - // Fetch issues and MRs for each filtered project - var projectIds = filteredProjects.map(p => p.id); - var issuesPromises = []; - var mrsPromises = []; - - projectIds.forEach(projectId => { - // Fetch issues - var issuesUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; - console.log(`[GitLab] Fetching issues for project ${projectId}:`, issuesUrl); - issuesPromises.push( - $.ajax({ - dataType: 'json', - type: 'GET', - url: issuesUrl, - success: function (data) { - console.log(`[GitLab][DEBUG] Raw issues response for project ${projectId}:`, data); - return data; - }, - error: function (xhr, textStatus, errorThrown) { - console.error(`[GitLab][ERROR] Issues API error for project ${projectId}:`, xhr.status, textStatus, errorThrown, xhr.responseText); - return []; - } - }) - ); - - // Fetch merge requests - var mrsUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; - console.log(`[GitLab] Fetching MRs for project ${projectId}:`, mrsUrl); - mrsPromises.push( - $.ajax({ - dataType: 'json', - type: 'GET', - url: mrsUrl - }) - ); - }); + // After fetching projects: + projects.forEach(p => { + gitlabProjectIdToName[p.id] = p.name; + }); - // Process all issues - Promise.all(issuesPromises) - .then(results => { - gitlabIssuesData = results.flat(); - console.log('[GitLab] Issues fetched (after flatten):', gitlabIssuesData); - writeGitlabIssuesPrs(); + // Fetch issues and MRs for each project + var projectIds = projects.map(p => p.id); + var issuesPromises = []; + var mrsPromises = []; + + projectIds.forEach(projectId => { + // Fetch issues + var issuesUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching issues for project ${projectId}:`, issuesUrl); + issuesPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: issuesUrl, + success: function (data) { + console.log(`[GitLab][DEBUG] Raw issues response for project ${projectId}:`, data); + return data; + }, + error: function (xhr, textStatus, errorThrown) { + console.error(`[GitLab][ERROR] Issues API error for project ${projectId}:`, xhr.status, textStatus, errorThrown, xhr.responseText); + return []; + } }) - .catch(error => { - console.error('Error fetching GitLab issues:', error); - Materialize.toast('Error fetching GitLab issues', 3000); - }); - - // Process all merge requests - Promise.all(mrsPromises) - .then(results => { - gitlabPrsReviewData = results.flat(); - console.log('[GitLab] Merge Requests fetched:', gitlabPrsReviewData.map(mr => ({ id: mr.id, title: mr.title, author: mr.author ? mr.author.username : undefined, project: mr.project_id ? gitlabProjectIdToName[mr.project_id] : undefined }))); - writeGitlabPrsReviews(); + ); + + // Fetch merge requests + var mrsUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; + console.log(`[GitLab] Fetching MRs for project ${projectId}:`, mrsUrl); + mrsPromises.push( + $.ajax({ + dataType: 'json', + type: 'GET', + url: mrsUrl }) - .catch(error => { - console.error('Error fetching GitLab merge requests:', error); - Materialize.toast('Error fetching GitLab merge requests', 3000); - }); - } - }); + ); + }); - // Fetch GitLab user data - var userUrl = `https://gitlab.com/api/v4/users?username=${gitlabUsername}`; - console.log('[GitLab] Fetching user data:', userUrl); - $.ajax({ - dataType: 'json', - type: 'GET', - url: userUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab user data:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown + // Process all issues + Promise.all(issuesPromises) + .then(results => { + gitlabIssuesData = results.flat(); + console.log('[GitLab] Issues fetched (after flatten):', gitlabIssuesData); + writeGitlabIssuesPrs(); + }) + .catch(error => { + console.error('Error fetching GitLab issues:', error); + Materialize.toast('Error fetching GitLab issues', 3000); }); - }, - success: (data) => { - if (data && data.length > 0) { - gitlabUserData = data[0]; - console.log('[GitLab] User data:', gitlabUserData); - } - }, - }); + + // Process all merge requests + Promise.all(mrsPromises) + .then(results => { + gitlabPrsReviewData = results.flat(); + console.log('[GitLab] Merge Requests fetched:', gitlabPrsReviewData.map(mr => ({ id: mr.id, title: mr.title, author: mr.author ? mr.author.username : undefined, project: mr.project ? mr.project.name : undefined }))); + writeGitlabPrsReviews(); + }) + .catch(error => { + console.error('Error fetching GitLab merge requests:', error); + Materialize.toast('Error fetching GitLab merge requests', 3000); + }); + } + }); + + // Fetch GitLab user data + var userUrl = `https://gitlab.com/api/v4/users?username=${gitlabUsername}`; + console.log('[GitLab] Fetching user data:', userUrl); + $.ajax({ + dataType: 'json', + type: 'GET', + url: userUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab user data:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown + }); + }, + success: (data) => { + if (data && data.length > 0) { + gitlabUserData = data[0]; + console.log('[GitLab] User data:', gitlabUserData); + } + }, }); } From 6082a6ea350b18bc82369f73bdd1cfbea2c42c7a Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 18 Jun 2025 18:18:34 +0530 Subject: [PATCH 04/40] fetches all gitlab activites --- src/scripts/scrumHelper.js | 126 ++++++++++++++----------------------- 1 file changed, 48 insertions(+), 78 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 2de8acc..96c28ea 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -559,94 +559,50 @@ ${userReason}`; } function fetchGitlabData() { - // First get user's projects - var projectsUrl = `https://gitlab.com/api/v4/users/${gitlabUsername}/projects?per_page=100`; - console.log('[GitLab] Fetching projects for user:', gitlabUsername, 'URL:', projectsUrl); + // First get user's activities directly + var issuesUrl = `https://gitlab.com/api/v4/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100&scope=all`; + console.log('[GitLab] Fetching all issues:', issuesUrl); + // Fetch all issues created by the user $.ajax({ dataType: 'json', type: 'GET', - url: projectsUrl, + url: issuesUrl, error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab projects:', { + console.error('Error fetching GitLab issues:', { status: xhr.status, textStatus: textStatus, error: errorThrown }); - Materialize.toast('Error fetching GitLab projects. Please check your username.', 3000); + Materialize.toast('Error fetching GitLab issues. Please check your username.', 3000); }, - success: (projects) => { - console.log('[GitLab] Projects fetched:', projects.map(p => ({ id: p.id, name: p.name, path_with_namespace: p.path_with_namespace }))); - if (!projects || projects.length === 0) { - Materialize.toast('No GitLab projects found for this user', 3000); - return; - } + success: (issues) => { + console.log('[GitLab] Issues fetched:', issues); + gitlabIssuesData = issues; + writeGitlabIssuesPrs(); + } + }); - // After fetching projects: - projects.forEach(p => { - gitlabProjectIdToName[p.id] = p.name; - }); + // Fetch all merge requests created by the user + var mrsUrl = `https://gitlab.com/api/v4/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100&scope=all`; + console.log('[GitLab] Fetching all MRs:', mrsUrl); - // Fetch issues and MRs for each project - var projectIds = projects.map(p => p.id); - var issuesPromises = []; - var mrsPromises = []; - - projectIds.forEach(projectId => { - // Fetch issues - var issuesUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; - console.log(`[GitLab] Fetching issues for project ${projectId}:`, issuesUrl); - issuesPromises.push( - $.ajax({ - dataType: 'json', - type: 'GET', - url: issuesUrl, - success: function (data) { - console.log(`[GitLab][DEBUG] Raw issues response for project ${projectId}:`, data); - return data; - }, - error: function (xhr, textStatus, errorThrown) { - console.error(`[GitLab][ERROR] Issues API error for project ${projectId}:`, xhr.status, textStatus, errorThrown, xhr.responseText); - return []; - } - }) - ); - - // Fetch merge requests - var mrsUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100`; - console.log(`[GitLab] Fetching MRs for project ${projectId}:`, mrsUrl); - mrsPromises.push( - $.ajax({ - dataType: 'json', - type: 'GET', - url: mrsUrl - }) - ); + $.ajax({ + dataType: 'json', + type: 'GET', + url: mrsUrl, + error: (xhr, textStatus, errorThrown) => { + console.error('Error fetching GitLab MRs:', { + status: xhr.status, + textStatus: textStatus, + error: errorThrown }); - - // Process all issues - Promise.all(issuesPromises) - .then(results => { - gitlabIssuesData = results.flat(); - console.log('[GitLab] Issues fetched (after flatten):', gitlabIssuesData); - writeGitlabIssuesPrs(); - }) - .catch(error => { - console.error('Error fetching GitLab issues:', error); - Materialize.toast('Error fetching GitLab issues', 3000); - }); - - // Process all merge requests - Promise.all(mrsPromises) - .then(results => { - gitlabPrsReviewData = results.flat(); - console.log('[GitLab] Merge Requests fetched:', gitlabPrsReviewData.map(mr => ({ id: mr.id, title: mr.title, author: mr.author ? mr.author.username : undefined, project: mr.project ? mr.project.name : undefined }))); - writeGitlabPrsReviews(); - }) - .catch(error => { - console.error('Error fetching GitLab merge requests:', error); - Materialize.toast('Error fetching GitLab merge requests', 3000); - }); + Materialize.toast('Error fetching GitLab merge requests.', 3000); + }, + success: (mrs) => { + console.log('[GitLab] MRs fetched:', mrs); + gitlabPrsReviewData = mrs; + writeGitlabPrsReviews(); } }); @@ -673,6 +629,20 @@ ${userReason}`; }); } + function getProjectName(item) { + // Get the project name from the full path + if (item.references?.full) { + return item.references.full.split('/').pop(); + } + if (item.project?.path_with_namespace) { + return item.project.path_with_namespace.split('/').pop(); + } + if (item.project?.name) { + return item.project.name; + } + return 'Unknown Project'; + } + function writeGitlabIssuesPrs() { if (!gitlabIssuesData) return; @@ -682,7 +652,7 @@ ${userReason}`; for (var i = 0; i < gitlabIssuesData.length; i++) { var item = gitlabIssuesData[i]; var web_url = item.web_url; - var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var project = getProjectName(item); var title = item.title; var iid = item.iid; var li = ''; @@ -710,7 +680,7 @@ ${userReason}`; for (var i = 0; i < gitlabPrsReviewData.length; i++) { var item = gitlabPrsReviewData[i]; var web_url = item.web_url; - var project = gitlabProjectIdToName[item.project_id] || 'Unknown Project'; + var project = getProjectName(item); var title = item.title; var iid = item.iid; @@ -722,7 +692,7 @@ ${userReason}`; number: iid, html_url: web_url, title: title, - state: item.state, + state: item.state }; gitlabPrsReviewDataProcessed[project].push(obj); } From 853383e0308feb1d4f32dbc6906973ab0994d0ef Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 19 Jun 2025 12:03:14 +0530 Subject: [PATCH 05/40] final merge and changes --- src/icons/settings-light.png | Bin 0 -> 23610 bytes src/icons/settings-night.png | Bin 0 -> 67359 bytes src/index.css | 221 +++++ src/manifest.json | 3 +- src/popup.html | 218 +++-- src/scripts/gitlabHelper.js | 253 +++++ src/scripts/main.js | 177 ++-- src/scripts/popup.js | 536 +++++++--- src/scripts/scrumHelper.js | 1778 ++++++++++++++++++++-------------- test.html | 203 ++++ 10 files changed, 2359 insertions(+), 1030 deletions(-) create mode 100644 src/icons/settings-light.png create mode 100644 src/icons/settings-night.png create mode 100644 src/scripts/gitlabHelper.js create mode 100644 test.html diff --git a/src/icons/settings-light.png b/src/icons/settings-light.png new file mode 100644 index 0000000000000000000000000000000000000000..de2325ee33fe47a5972d7579337b1bb5d7766cc6 GIT binary patch literal 23610 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 literal 0 HcmV?d00001 diff --git a/src/index.css b/src/index.css index ca33a72..6c007ab 100644 --- a/src/index.css +++ b/src/index.css @@ -117,4 +117,225 @@ body,input,div,h3,h4,p,label,hr, #scrumReport{ .dark-mode a { color: #00b7ff !important; +} +#refreshCache { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + border: none; + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); + transition: all 0.2s ease; +} + +#refreshCache:hover { + background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); + box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3); + transform: translateY(-1px); +} + +#refreshCache:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); +} + +#refreshCache i { + animation: spin 0s linear infinite; +} + +#refreshCache.loading i { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +/* Disabled state styling */ +.disabled-content { + opacity: 0.5 !important; + pointer-events: none !important; + user-select: none !important; +} + +.disabled-content input, +.disabled-content button, +.disabled-content [contenteditable] { + cursor: not-allowed !important; +} + +/* Dark mode disabled state */ +.dark-mode .disabled-content { + opacity: 0.4 !important; +} +.cache-info { + font-size: 11px; + color: #6b7280; + text-align: center; + margin-top: 8px; + line-height: 1.3; +} + +.cache-info i { + margin-right: 4px; + font-size: 10px; +} + +/* Dark mode for cache info */ +.dark-mode .cache-info { + color: #9ca3af; +} +#cacheInput { + transition: border-color 0.2s ease; +} + +#cacheInput:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Update the settings button styles: */ +#settingsToggle { + transition: all 0.2s ease; + border: none; + cursor: pointer; + background: none !important; +} + +#settingsToggle:hover { + transform: scale(1.05); + background: none !important; +} + +#settingsToggle:active { + transform: scale(0.95); +} + +#settingsToggle img { + transition: transform 0.2s ease; + filter: brightness(0.9); +} + +#settingsToggle.active img { + transform: rotate(45deg); + filter: brightness(1); +} + +.dark-mode #settingsToggle { + background: none !important; +} + +.dark-mode #settingsToggle:hover { + background: none !important; +} + +.dark-mode #settingsToggle img { + filter: brightness(0.9); +} + +.dark-mode #settingsToggle.active img { + filter: brightness(1); +} + +.tooltip-container { + position: relative; + display: inline-block; + margin-left: 4px; +} + +.question-icon { + color: #6b7280; + font-size: 14px; + cursor: help; + transition: color 0.2s ease; +} + +.question-icon:hover { + color: #3b82f6; +} + +.tooltip-bubble { + visibility: hidden; + opacity: 0; + position: absolute; + background-color: #1f2937; + color: white; + text-align: left; + padding: 12px; + border-radius: 8px; + font-size: 12px; + line-height: 1.4; + white-space: normal; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + z-index: 1000; + transition: opacity 0.3s ease, visibility 0.3s ease; + width: 280px; +} + +.tooltip-bubble { + bottom: 125%; + left: 150%; + transform: translateX(-50%); +} + +.tooltip-bubble::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #1f2937 transparent transparent transparent; +} + +.tooltip-container.tooltip-bottom .tooltip-bubble { + top: 125%; + bottom: auto; +} + +.tooltip-container.tooltip-bottom .tooltip-bubble::after { + top: -10px; + border-color: transparent transparent #1f2937 transparent; +} + +.tooltip-container.tooltip-right .tooltip-bubble { + top: -5px; + left: 125%; + bottom: auto; + transform: none; +} + +.tooltip-container.tooltip-right .tooltip-bubble::after { + top: 15px; + left: -10px; + border-color: transparent #1f2937 transparent transparent; +} + +.tooltip-container:hover .tooltip-bubble { + visibility: visible; + opacity: 1; +} + +.dark-mode .question-icon { + color: #9ca3af; +} + +.dark-mode .question-icon:hover { + color: #60a5fa; +} + +.dark-mode .tooltip-bubble { + background-color: #374151; + border: 1px solid #4b5563; +} + +.dark-mode .tooltip-bubble::after { + border-color: #374151 transparent transparent transparent; +} + +.dark-mode .tooltip-container.tooltip-bottom .tooltip-bubble::after { + border-color: transparent transparent #374151 transparent; +} + +.dark-mode .tooltip-container.tooltip-right .tooltip-bubble::after { + border-color: transparent #374151 transparent transparent; } \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index a04cb77..3baa7c4 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -57,6 +57,7 @@ "*://*.outlook.live.com/*", "*://*.office.com/*", "*://*.yahoo.com/*", - "https://api.github.com/*" + "https://api.github.com/*", + "https://gitlab.com/api/*" ] } \ No newline at end of file diff --git a/src/popup.html b/src/popup.html index 513318b..cc05141 100644 --- a/src/popup.html +++ b/src/popup.html @@ -31,124 +31,163 @@

    Scrum Helper

    Report your development progress by auto-fetching your Git activity for a selected period

    -
    -
    -
    -

    Your Project Name

    - -
    -
    -

    Platform

    -
    - - + +
    +
    +

    Your Project Name

    +
    -
    -
    -

    Your Github Username

    - -
    - -
    -

    Fetch your contributions between:

    -
    -
    - - +
    +

    Platform

    +
    + + +
    +
    + +
    +

    Your Github Username

    + +
    + + + +
    +

    Fetch your contributions between:

    +
    +
    + + +
    +
    + + +
    -
    - - + +
    +
    + + +
    +
    + + +
    -
    +
    +
    + + +
    +
    +

    What is blocking you from making progress?

    + +
    +
    +
    - - +
    Scrum Report
    +
    +
    -
    - - +
    + +
    -
    -
    - - -
    -
    -

    What is blocking you from making progress?

    - -
    -
    -
    -
    -
    Scrum Report
    -
    -
    + -
    -
    -

    Note:

    -
      -
    • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days - ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. - (See this issue.) -
    • -
    • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually - reviewing and editing the report to ensure accuracy before sharing -
    • -
    -
    + +
    +
    +

    Note:

    +
      +
    • The PRs fetched are based on the most recent review by any contributor. If you reviewed a PR 10 days + ago and someone else reviewed it 2 days ago, it will still appear in your activity for the past week. + (See this issue.) +
    • +
    • Please note that some discrepancies may occur in the generated SCRUM. We recommend manually reviewing + and editing the report to ensure accuracy before sharing +
    • +
    @@ -179,6 +218,7 @@

    Note:

    + diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js new file mode 100644 index 0000000..80e73bf --- /dev/null +++ b/src/scripts/gitlabHelper.js @@ -0,0 +1,253 @@ +// GitLab API Helper for Scrum Helper Extension +class GitLabHelper { + constructor() { + this.baseUrl = 'https://gitlab.com/api/v4'; + this.cache = { + data: null, + cacheKey: null, + timestamp: 0, + ttl: 10 * 60 * 1000, // 10 minutes + fetching: false, + queue: [] + }; + } + + async getCacheTTL() { + return new Promise((resolve) => { + chrome.storage.local.get(['cacheInput'], (items) => { + const ttl = items.cacheInput ? parseInt(items.cacheInput) * 60 * 1000 : 10 * 60 * 1000; + resolve(ttl); + }); + }); + } + + async saveToStorage(data) { + return new Promise((resolve) => { + chrome.storage.local.set({ + gitlabCache: { + data: data, + cacheKey: this.cache.cacheKey, + timestamp: this.cache.timestamp + } + }, resolve); + }); + } + + async loadFromStorage() { + return new Promise((resolve) => { + chrome.storage.local.get(['gitlabCache'], (items) => { + if (items.gitlabCache) { + this.cache.data = items.gitlabCache.data; + this.cache.cacheKey = items.gitlabCache.cacheKey; + this.cache.timestamp = items.gitlabCache.timestamp; + console.log('Restored GitLab cache from storage'); + } + resolve(); + }); + }); + } + + async fetchGitLabData(username, startDate, endDate) { + const cacheKey = `${username}-${startDate}-${endDate}`; + + if (this.cache.fetching || (this.cache.cacheKey === cacheKey && this.cache.data)) { + console.log('GitLab fetch already in progress or data already fetched. Skipping fetch.'); + return; + } + + console.log('Fetching GitLab data:', { + username: username, + startDate: startDate, + endDate: endDate, + }); + + // Check if we need to load from storage + if (!this.cache.data && !this.cache.fetching) { + await this.loadFromStorage(); + } + + const currentTTL = await this.getCacheTTL(); + this.cache.ttl = currentTTL; + console.log(`GitLab caching for ${currentTTL / (60 * 1000)} minutes`); + + const now = Date.now(); + const isCacheFresh = (now - this.cache.timestamp) < this.cache.ttl; + const isCacheKeyMatch = this.cache.cacheKey === cacheKey; + + if (this.cache.data && isCacheFresh && isCacheKeyMatch) { + console.log('Using cached GitLab data - cache is fresh and key matches'); + return this.cache.data; + } + + if (!isCacheKeyMatch) { + console.log('GitLab cache key mismatch - fetching new data'); + this.cache.data = null; + } else if (!isCacheFresh) { + console.log('GitLab cache is stale - fetching new data'); + } + + if (this.cache.fetching) { + console.log('GitLab fetch in progress, queuing requests'); + return new Promise((resolve, reject) => { + this.cache.queue.push({ resolve, reject }); + }); + } + + this.cache.fetching = true; + this.cache.cacheKey = cacheKey; + + try { + // Throttling 500ms to avoid burst + await new Promise(res => setTimeout(res, 500)); + + // Get user info first + const userUrl = `${this.baseUrl}/users?username=${username}`; + const userRes = await fetch(userUrl); + + if (!userRes.ok) { + throw new Error(`Error fetching GitLab user: ${userRes.status} ${userRes.statusText}`); + } + + const users = await userRes.json(); + if (users.length === 0) { + throw new Error(`GitLab user '${username}' not found`); + } + + const userId = users[0].id; + + // Fetch user's projects + const projectsUrl = `${this.baseUrl}/users/${userId}/projects?per_page=100&order_by=updated_at&sort=desc`; + const projectsRes = await fetch(projectsUrl); + + if (!projectsRes.ok) { + throw new Error(`Error fetching GitLab projects: ${projectsRes.status} ${projectsRes.statusText}`); + } + + const projects = await projectsRes.json(); + + // Fetch merge requests created by user + const mergeRequestsUrl = `${this.baseUrl}/merge_requests?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; + const mergeRequestsRes = await fetch(mergeRequestsUrl); + + if (!mergeRequestsRes.ok) { + throw new Error(`Error fetching GitLab merge requests: ${mergeRequestsRes.status} ${mergeRequestsRes.statusText}`); + } + + const mergeRequests = await mergeRequestsRes.json(); + + // Fetch issues created by user + const issuesUrl = `${this.baseUrl}/issues?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; + const issuesRes = await fetch(issuesUrl); + + if (!issuesRes.ok) { + throw new Error(`Error fetching GitLab issues: ${issuesRes.status} ${issuesRes.statusText}`); + } + + const issues = await issuesRes.json(); + + // Get detailed info for merge requests and issues + const detailedMergeRequests = await this.getDetailedMergeRequests(mergeRequests); + const detailedIssues = await this.getDetailedIssues(issues); + + const gitlabData = { + user: users[0], + projects: projects, + mergeRequests: detailedMergeRequests, + issues: detailedIssues, + comments: [] // Empty array since we're not fetching comments + }; + + // Cache the data + this.cache.data = gitlabData; + this.cache.timestamp = Date.now(); + + await this.saveToStorage(gitlabData); + + // Resolve queued calls + this.cache.queue.forEach(({ resolve }) => resolve(gitlabData)); + this.cache.queue = []; + + return gitlabData; + + } catch (err) { + console.error('GitLab Fetch Failed:', err); + // Reject queued calls on error + this.cache.queue.forEach(({ reject }) => reject(err)); + this.cache.queue = []; + throw err; + } finally { + this.cache.fetching = false; + } + } + + async getDetailedMergeRequests(mergeRequests) { + const detailed = []; + for (const mr of mergeRequests) { + try { + const url = `${this.baseUrl}/projects/${mr.project_id}/merge_requests/${mr.iid}`; + const res = await fetch(url); + if (res.ok) { + const detailedMr = await res.json(); + detailed.push(detailedMr); + } + // Add small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error fetching detailed MR ${mr.iid}:`, error); + detailed.push(mr); // Use basic data if detailed fetch fails + } + } + return detailed; + } + + async getDetailedIssues(issues) { + const detailed = []; + for (const issue of issues) { + try { + const url = `${this.baseUrl}/projects/${issue.project_id}/issues/${issue.iid}`; + const res = await fetch(url); + if (res.ok) { + const detailedIssue = await res.json(); + detailed.push(detailedIssue); + } + // Add small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error fetching detailed issue ${issue.iid}:`, error); + detailed.push(issue); // Use basic data if detailed fetch fails + } + } + return detailed; + } + + formatDate(dateString) { + const date = new Date(dateString); + const options = { day: '2-digit', month: 'short', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); + } + + processGitLabData(data) { + const processed = { + mergeRequests: data.mergeRequests || [], + issues: data.issues || [], + comments: data.comments || [], + user: data.user + }; + + console.log('GitLab data processed:', { + mergeRequests: processed.mergeRequests.length, + issues: processed.issues.length, + comments: processed.comments.length, + user: processed.user?.username + }); + + return processed; + } +} + +// Export for use in other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = GitLabHelper; +} else { + window.GitLabHelper = GitLabHelper; +} \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js index f805454..dff21fc 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,22 +1,18 @@ -var enableToggleElement = document.getElementById('enable'); -var githubUsernameElement = document.getElementById('githubUsername'); -var gitlabUsernameElement = document.getElementById('gitlabUsername'); -var projectNameElement = document.getElementById('projectName'); -var lastWeekContributionElement = document.getElementById('lastWeekContribution'); +let enableToggleElement = document.getElementById('enable'); +let githubUsernameElement = document.getElementById('githubUsername'); +let cacheInputElement = document.getElementById('cacheInput'); +let projectNameElement = document.getElementById('projectName'); +let lastWeekContributionElement = document.getElementById('lastWeekContribution'); let yesterdayContributionElement = document.getElementById('yesterdayContribution'); -var startingDateElement = document.getElementById('startingDate'); -var endingDateElement = document.getElementById('endingDate'); -var showOpenLabelElement = document.getElementById('showOpenLabel'); -var userReasonElement = document.getElementById('userReason'); -var platformRadios = document.getElementsByName('platform'); -var githubUsernameContainer = document.getElementById('githubUsernameContainer'); -var gitlabUsernameContainer = document.getElementById('gitlabUsernameContainer'); +let startingDateElement = document.getElementById('startingDate'); +let endingDateElement = document.getElementById('endingDate'); +let showOpenLabelElement = document.getElementById('showOpenLabel'); +let userReasonElement = document.getElementById('userReason'); function handleBodyOnLoad() { chrome.storage.local.get( [ 'githubUsername', - 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -26,22 +22,18 @@ function handleBodyOnLoad() { 'userReason', 'lastWeekContribution', 'yesterdayContribution', - 'platform', + 'cacheInput', ], (items) => { if (items.githubUsername) { githubUsernameElement.value = items.githubUsername; } - if (items.gitlabUsername) { - gitlabUsernameElement.value = items.gitlabUsername; - } - if (items.platform) { - document.querySelector(`input[name="platform"][value="${items.platform}"]`).checked = true; - handlePlatformChange(items.platform); - } if (items.projectName) { projectNameElement.value = items.projectName; } + if (items.cacheInput) { + cacheInputElement.value = items.cacheInput; + } if (items.enableToggle) { enableToggleElement.checked = items.enableToggle; } else if (items.enableToggle !== false) { @@ -69,7 +61,7 @@ function handleBodyOnLoad() { lastWeekContributionElement.checked = items.lastWeekContribution; handleLastWeekContributionChange(); } - else if (items.lastWeekContribution !== false) { + else if (items.lastWeekContribution !== false) { lastWeekContributionElement.checked = true; handleLastWeekContributionChange(); } @@ -77,45 +69,71 @@ function handleBodyOnLoad() { yesterdayContributionElement.checked = items.yesterdayContribution; handleYesterdayContributionChange(); } - else if (items.yesterdayContribution !== false) { + else if (items.yesterdayContribution !== false) { yesterdayContributionElement.checked = true; handleYesterdayContributionChange(); } }, ); } + +document.getElementById('refreshCache').addEventListener('click', async (e) => { + const button = e.currentTarget; + button.classList.add('loading'); + button.disabled = true; + + try { + const tabs = await chrome.tabs.query({active: true, currentWindow: true}); + await chrome.tabs.sendMessage(tabs[0].id, { + action: 'forceRefresh', + timestamp: Date.now() + }); + + // Reload the active tab to re-inject content + chrome.tabs.reload(tabs[0].id); + + Materialize.toast({html: 'Data refreshed successfully!', classes: 'green'}); + } catch (err) { + console.error('Refresh failed:', err); + } finally { + setTimeout(() => { + button.classList.remove('loading'); + button.disabled = false; + }, 500); + } +}); + function handleEnableChange() { - var value = enableToggleElement.checked; + let value = enableToggleElement.checked; chrome.storage.local.set({ enableToggle: value }); } function handleStartingDateChange() { - var value = startingDateElement.value; + let value = startingDateElement.value; chrome.storage.local.set({ startingDate: value }); } function handleEndingDateChange() { - var value = endingDateElement.value; + let value = endingDateElement.value; chrome.storage.local.set({ endingDate: value }); } function handleLastWeekContributionChange() { - var value = lastWeekContributionElement.checked; - var labelElement = document.querySelector("label[for='lastWeekContribution']"); - + let value = lastWeekContributionElement.checked; + let labelElement = document.querySelector("label[for='lastWeekContribution']"); if (value) { - startingDateElement.disabled = true; - endingDateElement.disabled = true; - endingDateElement.value = getToday(); - startingDateElement.value = getLastWeek(); - handleEndingDateChange(); - handleStartingDateChange(); - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + startingDateElement.disabled = true; + endingDateElement.disabled = true; + endingDateElement.value = getToday(); + startingDateElement.value = getLastWeek(); + handleEndingDateChange(); + handleStartingDateChange(); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - startingDateElement.disabled = false; - endingDateElement.disabled = false; - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + startingDateElement.disabled = false; + endingDateElement.disabled = false; + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } - + chrome.storage.local.set({ lastWeekContribution: value }); } @@ -128,8 +146,8 @@ function handleYesterdayContributionChange() { endingDateElement.disabled = true; endingDateElement.value = getToday(); startingDateElement.value = getYesterday(); - handleEndingDateChange(); - handleStartingDateChange(); + handleEndingDateChange(); + handleStartingDateChange(); labelElement.classList.add("selectedLabel"); labelElement.classList.remove("unselectedLabel"); } else { @@ -142,12 +160,12 @@ function handleYesterdayContributionChange() { } function getLastWeek() { - var today = new Date(); - var lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - var lastWeekMonth = lastWeek.getMonth() + 1; - var lastWeekDay = lastWeek.getDate(); - var lastWeekYear = lastWeek.getFullYear(); - var lastWeekDisplayPadded = + let today = new Date(); + let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + let lastWeekMonth = lastWeek.getMonth() + 1; + let lastWeekDay = lastWeek.getDate(); + let lastWeekYear = lastWeek.getFullYear(); + let lastWeekDisplayPadded = ('0000' + lastWeekYear.toString()).slice(-4) + '-' + ('00' + lastWeekMonth.toString()).slice(-2) + @@ -161,7 +179,7 @@ function getYesterday() { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayWeekDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -170,12 +188,12 @@ function getYesterday() { return yesterdayPadded; } function getToday() { - var today = new Date(); - var Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - var WeekMonth = Week.getMonth() + 1; - var WeekDay = Week.getDate(); - var WeekYear = Week.getFullYear(); - var WeekDisplayPadded = + let today = new Date(); + let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + let WeekMonth = Week.getMonth() + 1; + let WeekDay = Week.getDate(); + let WeekYear = Week.getFullYear(); + let WeekDisplayPadded = ('0000' + WeekYear.toString()).slice(-4) + '-' + ('00' + WeekMonth.toString()).slice(-2) + @@ -185,52 +203,39 @@ function getToday() { } function handleGithubUsernameChange() { - var value = githubUsernameElement.value; + let value = githubUsernameElement.value; chrome.storage.local.set({ githubUsername: value }); } function handleProjectNameChange() { - var value = projectNameElement.value; + let value = projectNameElement.value; chrome.storage.local.set({ projectName: value }); } +function handleCacheInputChange() { + let value = cacheInputElement.value; + chrome.storage.local.set({ cacheInput: value }); +} function handleOpenLabelChange() { - var value = showOpenLabelElement.checked; - var labelElement = document.querySelector("label[for='showOpenLabel']"); + let value = showOpenLabelElement.checked; + let labelElement = document.querySelector("label[for='showOpenLabel']"); if (value) { - labelElement.classList.add("selectedLabel"); - labelElement.classList.remove("unselectedLabel"); + labelElement.classList.add("selectedLabel"); + labelElement.classList.remove("unselectedLabel"); } else { - labelElement.classList.add("unselectedLabel"); - labelElement.classList.remove("selectedLabel"); + labelElement.classList.add("unselectedLabel"); + labelElement.classList.remove("selectedLabel"); } chrome.storage.local.set({ showOpenLabel: value }); } function handleUserReasonChange() { - var value = userReasonElement.value; + let value = userReasonElement.value; chrome.storage.local.set({ userReason: value }); } - -function handlePlatformChange(platform) { - chrome.storage.local.set({ platform: platform }); - - if (platform === 'github') { - githubUsernameContainer.classList.remove('hidden'); - gitlabUsernameContainer.classList.add('hidden'); - } else { - githubUsernameContainer.classList.add('hidden'); - gitlabUsernameContainer.classList.remove('hidden'); - } -} - -function handleGitlabUsernameChange() { - var value = gitlabUsernameElement.value; - chrome.storage.local.set({ gitlabUsername: value }); -} - enableToggleElement.addEventListener('change', handleEnableChange); githubUsernameElement.addEventListener('keyup', handleGithubUsernameChange); +cacheInputElement.addEventListener('keyup', handleCacheInputChange); projectNameElement.addEventListener('keyup', handleProjectNameChange); startingDateElement.addEventListener('change', handleStartingDateChange); endingDateElement.addEventListener('change', handleEndingDateChange); @@ -238,8 +243,4 @@ lastWeekContributionElement.addEventListener('change', handleLastWeekContributio yesterdayContributionElement.addEventListener('change', handleYesterdayContributionChange); showOpenLabelElement.addEventListener('change', handleOpenLabelChange); userReasonElement.addEventListener('keyup', handleUserReasonChange); -platformRadios.forEach(radio => { - radio.addEventListener('change', (e) => handlePlatformChange(e.target.value)); -}); -gitlabUsernameElement.addEventListener('keyup', handleGitlabUsernameChange); document.addEventListener('DOMContentLoaded', handleBodyOnLoad); \ No newline at end of file diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 254a1d6..0449338 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -1,10 +1,10 @@ function getLastWeek() { - var today = new Date(); - var lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - var lastWeekMonth = lastWeek.getMonth() + 1; - var lastWeekDay = lastWeek.getDate(); - var lastWeekYear = lastWeek.getFullYear(); - var lastWeekDisplayPadded = + let today = new Date(); + let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + let lastWeekMonth = lastWeek.getMonth() + 1; + let lastWeekDay = lastWeek.getDate(); + let lastWeekYear = lastWeek.getFullYear(); + let lastWeekDisplayPadded = ('0000' + lastWeekYear.toString()).slice(-4) + '-' + ('00' + lastWeekMonth.toString()).slice(-2) + @@ -14,11 +14,11 @@ function getLastWeek() { } function getToday() { - var today = new Date(); - var WeekMonth = today.getMonth() + 1; - var WeekDay = today.getDate(); - var WeekYear = today.getFullYear(); - var WeekDisplayPadded = + let today = new Date(); + let WeekMonth = today.getMonth() + 1; + let WeekDay = today.getDate(); + let WeekYear = today.getFullYear(); + let WeekDisplayPadded = ('0000' + WeekYear.toString()).slice(-4) + '-' + ('00' + WeekMonth.toString()).slice(-2) + @@ -33,7 +33,7 @@ function getYesterday() { let yesterdayMonth = yesterday.getMonth() + 1; let yesterdayDay = yesterday.getDate(); let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = + let yesterdayPadded = ('0000' + yesterdayYear.toString()).slice(-4) + '-' + ('00' + yesterdayMonth.toString()).slice(-2) + @@ -42,151 +42,418 @@ function getYesterday() { return yesterdayPadded; } -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { // Dark mode setup const darkModeToggle = document.querySelector('img[alt="Night Mode"]'); + const settingsIcon = document.getElementById('settingsIcon'); const body = document.body; - chrome.storage.local.get(['darkMode'], function(result) { - if(result.darkMode) { + chrome.storage.local.get(['darkMode'], function (result) { + if (result.darkMode) { body.classList.add('dark-mode'); darkModeToggle.src = 'icons/light-mode.png'; + if (settingsIcon) { + settingsIcon.src = 'icons/settings-night.png'; + } + } else { + if (settingsIcon) { + settingsIcon.src = 'icons/settings-light.png'; + } } }); - darkModeToggle.addEventListener('click', function() { + darkModeToggle.addEventListener('click', function () { body.classList.toggle('dark-mode'); const isDarkMode = body.classList.contains('dark-mode'); chrome.storage.local.set({ darkMode: isDarkMode }); this.src = isDarkMode ? 'icons/light-mode.png' : 'icons/night-mode.png'; + const settingsIcon = document.getElementById('settingsIcon'); + if (settingsIcon) { + settingsIcon.src = isDarkMode ? 'icons/settings-night.png' : 'icons/settings-light.png'; + } }); - // Button setup - const generateBtn = document.getElementById('generateReport'); - const copyBtn = document.getElementById('copyReport'); - - generateBtn.addEventListener('click', function() { - this.innerHTML = ' Generating...'; - this.disabled = true; - window.generateScrumReport(); + // Platform selection setup for checkboxes + const githubCheckbox = document.getElementById('platformGithub'); + const gitlabCheckbox = document.getElementById('platformGitlab'); + const githubUsernameContainer = document.getElementById('githubUsernameContainer'); + const gitlabUsernameContainer = document.getElementById('gitlabUsernameContainer'); + + // Load saved platform preference + chrome.storage.local.get(['selectedPlatform'], function (result) { + const platform = result.selectedPlatform || 'github'; + selectPlatform(platform); }); - - copyBtn.addEventListener('click', function() { - const scrumReport = document.getElementById('scrumReport'); - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = scrumReport.innerHTML; - document.body.appendChild(tempDiv); - tempDiv.style.position = 'absolute'; - tempDiv.style.left = '-9999px'; - - const range = document.createRange(); - range.selectNode(tempDiv); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - try { - document.execCommand('copy'); - this.innerHTML = ' Copied!'; - setTimeout(() => { - this.innerHTML = ' Copy Report'; - }, 2000); - } catch (err) { - console.error('Failed to copy: ', err); - } finally { - selection.removeAllRanges(); - document.body.removeChild(tempDiv); + + function selectPlatform(platform) { + if (platform === 'github') { + githubCheckbox.checked = true; + gitlabCheckbox.checked = false; + githubUsernameContainer.classList.remove('hidden'); + gitlabUsernameContainer.classList.add('hidden'); + chrome.storage.local.remove(['gitlabCache']); + } else { + gitlabCheckbox.checked = true; + githubCheckbox.checked = false; + gitlabUsernameContainer.classList.remove('hidden'); + githubUsernameContainer.classList.add('hidden'); + chrome.storage.local.remove(['githubCache']); + } + chrome.storage.local.set({ selectedPlatform: platform }); + } + + githubCheckbox.addEventListener('change', function () { + if (githubCheckbox.checked) { + selectPlatform('github'); + } else if (!gitlabCheckbox.checked) { + // Prevent both from being unchecked + githubCheckbox.checked = true; } }); + gitlabCheckbox.addEventListener('change', function () { + if (gitlabCheckbox.checked) { + selectPlatform('gitlab'); + } else if (!githubCheckbox.checked) { + // Prevent both from being unchecked + gitlabCheckbox.checked = true; + } + }); + + function updateContentState(enableToggle) { + const elementsToToggle = [ + 'startingDate', + 'endingDate', + 'userReason', + 'generateReport', + 'copyReport', + 'refreshCache', + 'showOpenLabel', + 'scrumReport', + 'githubUsername', + 'gitlabUsername', + 'projectName', + 'settingsToggle', + 'githubCheckbox', + 'gitlabCheckbox' + ]; + + const radios = document.querySelectorAll('input[name="timeframe"]'); + const customDateContainer = document.getElementById('customDateContainer'); - // Custom date container click handler - document.getElementById('customDateContainer').addEventListener('click', () => { - document.querySelectorAll('input[name="timeframe"]').forEach(radio => { - radio.checked = false - radio.dataset.wasChecked = 'false' + elementsToToggle.forEach(id => { + const element = document.getElementById(id); + if (element) { + element.disabled = !enableToggle; + if (!enableToggle) { + element.style.opacity = '0.5'; + element.style.pointerEvents = 'none'; + } else { + element.style.opacity = '1'; + element.style.pointerEvents = 'auto'; + } + } }); - - const startDateInput = document.getElementById('startingDate'); - const endDateInput = document.getElementById('endingDate'); - startDateInput.disabled = false; - endDateInput.disabled = false; - - chrome.storage.local.set({ - lastWeekContribution: false, - yesterdayContribution: false, - selectedTimeframe: null + + radios.forEach(radio => { + radio.disabled = !enableToggle; + const label = document.querySelector(`label[for="${radio.id}"]`); + if (label) { + if (!enableToggle) { + label.style.opacity = '0.5'; + label.style.pointerEvents = 'none'; + } else { + label.style.opacity = '1'; + label.style.pointerEvents = 'auto'; + } + } }); - }); - chrome.storage.local.get([ - 'selectedTimeframe', - 'lastWeekContribution', - 'yesterdayContribution' - ], (items) => { - console.log('Restoring state:', items); - - if (!items.selectedTimeframe) { - items.selectedTimeframe = 'yesterdayContribution'; - items.lastWeekContribution = false; - items.yesterdayContribution = true; - } - - const radio = document.getElementById(items.selectedTimeframe); - if (radio) { - radio.checked = true; - radio.dataset.wasChecked = 'true'; - - const startDateInput = document.getElementById('startingDate'); - const endDateInput = document.getElementById('endingDate'); - if (items.selectedTimeframe === 'lastWeekContribution') { - startDateInput.value = getLastWeek(); - endDateInput.value = getToday(); + if (customDateContainer) { + if (!enableToggle) { + customDateContainer.style.opacity = '0.5'; + customDateContainer.style.pointerEvents = 'none'; } else { - startDateInput.value = getYesterday(); - endDateInput.value = getToday(); + customDateContainer.style.opacity = '1'; + customDateContainer.style.pointerEvents = 'auto'; } + } - startDateInput.disabled = endDateInput.disabled = true; + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.contentEditable = enableToggle; + if (!enableToggle) { + scrumReport.innerHTML = '

    Extension is disabled. Enable it from the options to generate scrum reports.

    '; + } else { + const disabledMessage = '

    Extension is disabled. Enable it from the options to generate scrum reports.

    '; + if (scrumReport.innerHTML === disabledMessage) { + scrumReport.innerHTML = ''; + } + } + } + } - chrome.storage.local.set({ - startingDate: startDateInput.value, - endingDate: endDateInput.value, - lastWeekContribution: items.selectedTimeframe === 'lastWeekContribution', - yesterdayContribution: items.selectedTimeframe === 'yesterdayContribution', - selectedTimeframe: items.selectedTimeframe - }); + chrome.storage.local.get(['enableToggle'], (items) => { + const enableToggle = items.enableToggle !== false; + updateContentState(enableToggle); + if (!enableToggle) { + return; + } + + initializePopup(); + }) + + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes.enableToggle) { + updateContentState(changes.enableToggle.newValue); + if (changes.enableToggle.newValue) { + // re-initialize if enabled + initializePopup(); + } } }); - // Radio button click handlers with toggle functionality - document.querySelectorAll('input[name="timeframe"]').forEach(radio => { - radio.addEventListener('click', function() { - if (this.dataset.wasChecked === 'true') { - this.checked = false; - this.dataset.wasChecked = 'false'; - + function initializePopup() { + + // Button setup + const generateBtn = document.getElementById('generateReport'); + const copyBtn = document.getElementById('copyReport'); + + generateBtn.addEventListener('click', function () { + this.innerHTML = ' Generating...'; + this.disabled = true; + window.generateScrumReport(); + }); + + copyBtn.addEventListener('click', function () { + const scrumReport = document.getElementById('scrumReport'); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = scrumReport.innerHTML; + document.body.appendChild(tempDiv); + tempDiv.style.position = 'absolute'; + tempDiv.style.left = '-9999px'; + + const range = document.createRange(); + range.selectNode(tempDiv); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + try { + document.execCommand('copy'); + this.innerHTML = ' Copied!'; + setTimeout(() => { + this.innerHTML = ' Copy Report'; + }, 2000); + } catch (err) { + console.error('Failed to copy: ', err); + } finally { + selection.removeAllRanges(); + document.body.removeChild(tempDiv); + } + }); + + // Username input event listeners + const githubUsernameInput = document.getElementById('githubUsername'); + const gitlabUsernameInput = document.getElementById('gitlabUsername'); + const projectNameInput = document.getElementById('projectName'); + + if (githubUsernameInput) { + githubUsernameInput.addEventListener('blur', function () { + chrome.storage.local.set({ githubUsername: this.value }); + }); + } + + if (gitlabUsernameInput) { + gitlabUsernameInput.addEventListener('blur', function () { + chrome.storage.local.set({ gitlabUsername: this.value }); + }); + } + + if (projectNameInput) { + projectNameInput.addEventListener('blur', function () { + chrome.storage.local.set({ projectName: this.value }); + }); + } + + // Load saved usernames and project name + chrome.storage.local.get(['githubUsername', 'gitlabUsername', 'projectName'], function (items) { + if (items.githubUsername && githubUsernameInput) { + githubUsernameInput.value = items.githubUsername; + } + if (items.gitlabUsername && gitlabUsernameInput) { + gitlabUsernameInput.value = items.gitlabUsername; + } + if (items.projectName && projectNameInput) { + projectNameInput.value = items.projectName; + } + }); + + // Custom date container click handler + document.getElementById('customDateContainer').addEventListener('click', () => { + document.querySelectorAll('input[name="timeframe"]').forEach(radio => { + radio.checked = false + radio.dataset.wasChecked = 'false' + }); + + const startDateInput = document.getElementById('startingDate'); + const endDateInput = document.getElementById('endingDate'); + startDateInput.disabled = false; + endDateInput.disabled = false; + + chrome.storage.local.set({ + lastWeekContribution: false, + yesterdayContribution: false, + selectedTimeframe: null + }); + }); + + chrome.storage.local.get([ + 'selectedTimeframe', + 'lastWeekContribution', + 'yesterdayContribution' + ], (items) => { + console.log('Restoring state:', items); + + if (!items.selectedTimeframe) { + items.selectedTimeframe = 'yesterdayContribution'; + items.lastWeekContribution = false; + items.yesterdayContribution = true; + } + + const radio = document.getElementById(items.selectedTimeframe); + if (radio) { + radio.checked = true; + radio.dataset.wasChecked = 'true'; + const startDateInput = document.getElementById('startingDate'); const endDateInput = document.getElementById('endingDate'); - startDateInput.disabled = false; - endDateInput.disabled = false; - + + if (items.selectedTimeframe === 'lastWeekContribution') { + startDateInput.value = getLastWeek(); + endDateInput.value = getToday(); + } else { + startDateInput.value = getYesterday(); + endDateInput.value = getToday(); + } + + startDateInput.disabled = endDateInput.disabled = true; + chrome.storage.local.set({ - lastWeekContribution: false, - yesterdayContribution: false, - selectedTimeframe: null + startingDate: startDateInput.value, + endingDate: endDateInput.value, + lastWeekContribution: items.selectedTimeframe === 'lastWeekContribution', + yesterdayContribution: items.selectedTimeframe === 'yesterdayContribution', + selectedTimeframe: items.selectedTimeframe }); + } + }); + } + + const settingsToggle = document.getElementById('settingsToggle'); + const reportSection = document.getElementById('reportSection'); + const settingsSection = document.getElementById('settingsSection'); + + let isSettingsVisible = false; + + function showReportView() { + isSettingsVisible = false; + reportSection.classList.remove('hidden'); + settingsSection.classList.add('hidden'); + settingsToggle.classList.remove('active'); + console.log('Switched to report view'); + } + + function showSettingsView() { + isSettingsVisible = true; + reportSection.classList.add('hidden'); + settingsSection.classList.remove('hidden'); + settingsToggle.classList.add('active'); + console.log('Switched to settings view'); + } + + if (settingsToggle) { + settingsToggle.addEventListener('click', function () { + if (isSettingsVisible) { + showReportView(); } else { - document.querySelectorAll('input[name="timeframe"]').forEach(r => { - r.dataset.wasChecked = 'false'; - }); - this.dataset.wasChecked = 'true'; - toggleRadio(this); + showSettingsView(); } }); + } + + showReportView(); + +}); + +// Radio button click handlers with toggle functionality +document.querySelectorAll('input[name="timeframe"]').forEach(radio => { + radio.addEventListener('click', function () { + if (this.dataset.wasChecked === 'true') { + this.checked = false; + this.dataset.wasChecked = 'false'; + + const startDateInput = document.getElementById('startingDate'); + const endDateInput = document.getElementById('endingDate'); + startDateInput.disabled = false; + endDateInput.disabled = false; + + chrome.storage.local.set({ + lastWeekContribution: false, + yesterdayContribution: false, + selectedTimeframe: null + }); + } else { + document.querySelectorAll('input[name="timeframe"]').forEach(r => { + r.dataset.wasChecked = 'false'; + }); + this.dataset.wasChecked = 'true'; + toggleRadio(this); + } }); }); +// refresh cache button +document.getElementById('refreshCache').addEventListener('click', async function () { + const button = this; + const originalText = button.innerHTML; + + button.classList.add('loading'); + button.innerHTML = 'Refreshing...'; + button.disabled = true; + + try { + // Clear local cache for both platforms + await new Promise(resolve => { + chrome.storage.local.remove(['githubCache', 'gitlabCache'], resolve); + }); + + // Clear the scrum report + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = '

    Cache cleared successfully. Click "Generate Report" to fetch fresh data.

    '; + } + + button.innerHTML = 'Cache Cleared!'; + button.classList.remove('loading'); + + setTimeout(() => { + button.innerHTML = originalText; + button.disabled = false; + }, 2000); + + } catch (error) { + console.error('Cache clear failed:', error); + button.innerHTML = 'Failed to clear cache'; + button.classList.remove('loading'); + + setTimeout(() => { + button.innerHTML = originalText; + button.disabled = false; + }, 3000); + } +}); + function toggleRadio(radio) { const startDateInput = document.getElementById('startingDate'); const endDateInput = document.getElementById('endingDate'); @@ -209,13 +476,44 @@ function toggleRadio(radio) { lastWeekContribution: radio.id === 'lastWeekContribution', yesterdayContribution: radio.id === 'yesterdayContribution', selectedTimeframe: radio.id, - githubCache: null // Clear cache to force new fetch + githubCache: null, // Clear cache to force new fetch + gitlabCache: null // Clear GitLab cache to force new fetch }, () => { console.log('State saved, dates:', { start: startDateInput.value, end: endDateInput.value, isLastWeek: radio.id === 'lastWeekContribution' }); - // window.generateScrumReport(); }); +} + +const cacheInput = document.getElementById('cacheInput'); +if (cacheInput) { + chrome.storage.local.get(['cacheInputValue'], function (result) { + if (result.cacheInputValue) { + cacheInput.value = result.cacheInputValue; + } else { + cacheInput.value = 10; + } + }); + + cacheInput.addEventListener('blur', function () { + let ttlValue = parseInt(this.value); + if (isNaN(ttlValue) || ttlValue <= 0 || this.value.trim() === '') { + ttlValue = 10; + this.value = ttlValue; + this.style.borderColor = '#ef4444'; + } else if (ttlValue > 1440) { + ttlValue = 1440; + this.value = ttlValue; + this.style.borderColor = '#f59e0b'; + } else { + this.style.borderColor = '#10b981'; + } + + chrome.storage.local.set({ cacheInputValue: ttlValue }, function () { + console.log('Cache TTL saved:', ttlValue, 'minutes'); + }); + }); + } \ No newline at end of file diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 96c28ea..374e676 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1,750 +1,1062 @@ +// GitLab support added + console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); -var enableToggle = true; -var gitlabProjectIdToName = {}; +let refreshButton_Placed = false; +let enableToggle = true; +let hasInjectedContent = false; function allIncluded(outputTarget = 'email') { - console.log('allIncluded called with outputTarget:', outputTarget); - console.log('Current window context:', window.location.href); - /* global $*/ - var scrumBody = null; - var scrumSubject = null; - var startingDate = ''; - var endingDate = ''; - var githubUsername = ''; - var gitlabUsername = ''; - var projectName = ''; - var platform = 'github'; - var lastWeekArray = []; - var nextWeekArray = []; - var reviewedPrsArray = []; - var githubIssuesData = null; - var gitlabIssuesData = null; - var lastWeekContribution = false; - let yesterdayContribution = false; - var githubPrsReviewData = null; - var gitlabPrsReviewData = null; - var githubUserData = null; - var gitlabUserData = null; - var githubPrsReviewDataProcessed = {}; - var gitlabPrsReviewDataProcessed = {}; - var showOpenLabel = true; - var showClosedLabel = true; - var userReason = ''; - - var pr_merged_button = - '
    closed
    '; - var pr_unmerged_button = - '
    open
    '; - - var issue_closed_button = - '
    closed
    '; - var issue_opened_button = - '
    open
    '; - - // var linkStyle = ''; - function getChromeData() { - console.log("Getting Chrome data for context:", outputTarget); - chrome.storage.local.get( - [ - 'githubUsername', - 'gitlabUsername', - 'projectName', - 'enableToggle', - 'startingDate', - 'endingDate', - 'showOpenLabel', - 'showClosedLabel', - 'lastWeekContribution', - 'yesterdayContribution', - 'userReason', - 'platform', - ], - (items) => { - console.log("Storage items received:", items); - - if (items.platform) { - platform = items.platform; - } - - if (items.lastWeekContribution) { - lastWeekContribution = true; - handleLastWeekContributionChange(); - } - if (items.yesterdayContribution) { - yesterdayContribution = true; - handleYesterdayContributionChange(); - } - if (!items.enableToggle) { - enableToggle = items.enableToggle; - } - if (items.endingDate && !lastWeekContribution) { - endingDate = items.endingDate; - } - if (items.startingDate && !lastWeekContribution) { - startingDate = items.startingDate; - } - if (items.endingDate && !yesterdayContribution) { - endingDate = items.endingDate; - } - if (items.startingDate && !yesterdayContribution) { - startingDate = items.startingDate; - } - - if (platform === 'github') { - if (items.githubUsername) { - githubUsername = items.githubUsername; - console.log("About to fetch GitHub data for:", githubUsername); - fetchGithubData(); - } else { - if (outputTarget === 'popup') { - console.log("No GitHub username found - popup context"); - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - Materialize.toast('Please enter your GitHub username', 3000); - } else { - console.log("No GitHub username found - email context"); - console.warn('No GitHub username found in storage'); - } - } - } else { - if (items.gitlabUsername) { - gitlabUsername = items.gitlabUsername; - console.log("About to fetch GitLab data for:", gitlabUsername); - fetchGitlabData(); - } else { - if (outputTarget === 'popup') { - console.log("No GitLab username found - popup context"); - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - Materialize.toast('Please enter your GitLab username', 3000); - } else { - console.log("No GitLab username found - email context"); - console.warn('No GitLab username found in storage'); - } - } - } - - if (items.projectName) { - projectName = items.projectName; - } - - if (!items.showOpenLabel) { - showOpenLabel = false; - pr_unmerged_button = ''; - issue_opened_button = ''; - } - if (!items.showClosedLabel) { - showClosedLabel = false; - pr_merged_button = ''; - issue_closed_button = ''; - } - if (items.userReason) { - userReason = items.userReason; - } - if (!items.userReason) { - userReason = 'No Blocker at the moment'; - } - }, - ); - } - getChromeData(); - - function handleLastWeekContributionChange() { - endingDate = getToday(); - startingDate = getLastWeek(); - } - function handleYesterdayContributionChange() { - endingDate = getToday(); - startingDate = getYesterday(); - } - function getLastWeek() { - var today = new Date(); - var lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - var lastWeekMonth = lastWeek.getMonth() + 1; - var lastWeekDay = lastWeek.getDate(); - var lastWeekYear = lastWeek.getFullYear(); - var lastWeekDisplayPadded = - ('0000' + lastWeekYear.toString()).slice(-4) + - '-' + - ('00' + lastWeekMonth.toString()).slice(-2) + - '-' + - ('00' + lastWeekDay.toString()).slice(-2); - return lastWeekDisplayPadded; - } - function getYesterday() { - let today = new Date(); - let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - let yesterdayMonth = yesterday.getMonth() + 1; - let yesterdayWeekDay = yesterday.getDate(); - let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = - ('0000' + yesterdayYear.toString()).slice(-4) + - '-' + - ('00' + yesterdayMonth.toString()).slice(-2) + - '-' + - ('00' + yesterdayWeekDay.toString()).slice(-2); - return yesterdayPadded; - } - function getToday() { - var today = new Date(); - var Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - var WeekMonth = Week.getMonth() + 1; - var WeekDay = Week.getDate(); - var WeekYear = Week.getFullYear(); - var WeekDisplayPadded = - ('0000' + WeekYear.toString()).slice(-4) + - '-' + - ('00' + WeekMonth.toString()).slice(-2) + - '-' + - ('00' + WeekDay.toString()).slice(-2); - return WeekDisplayPadded; - } - // fetch github data - function fetchGithubData() { - var issueUrl = 'https://api.github.com/search/issues?q=author%3A' + - githubUsername + - '+org%3Afossasia+created%3A' + - startingDate + - '..' + - endingDate + - '&per_page=100'; - - $.ajax({ - dataType: 'json', - type: 'GET', - url: issueUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitHub data:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - }, - success: (data) => { - githubIssuesData = data; - writeGithubIssuesPrs(); - }, - }); - - // PR reviews fetch - var prUrl = 'https://api.github.com/search/issues?q=commenter%3A' + - githubUsername + - '+org%3Afossasia+updated%3A' + - startingDate + - '..' + - endingDate + - '&per_page=100'; - - $.ajax({ - dataType: 'json', - type: 'GET', - url: prUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching PR reviews:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - }, - success: (data) => { - githubPrsReviewData = data; - writeGithubPrsReviews(); - }, - }); - // fetch github user data - var userUrl = 'https://api.github.com/users/' + githubUsername; - $.ajax({ - dataType: 'json', - type: 'GET', - url: userUrl, - error: (xhr, textStatus, errorThrown) => { - // error - }, - success: (data) => { - githubUserData = data; - }, - }); - } - - function formatDate(dateString) { - const date = new Date(dateString); - const options = { day: '2-digit', month: 'short', year: 'numeric' }; - return date.toLocaleDateString('en-US', options); - } - - //load initial text in scrum body - function writeScrumBody() { - if (!enableToggle) return; - - if (outputTarget === 'email') { - if (!window.emailClientAdapter) { - console.error('Email client adapter not found'); - return; - } - if (!window.emailClientAdapter.isNewConversation()) { - console.log('Not a new conversation, skipping scrum helper'); - return; - } - } - - setTimeout(() => { - // Generate content first - var lastWeekUl = '
      '; - var i; - for (i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; - for (i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; - lastWeekUl += '
    '; - - var nextWeekUl = '
      '; - for (i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; - nextWeekUl += '
    '; - - var weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); - var weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; - - // Create the complete content - let content; - if (lastWeekContribution == true || yesterdayContribution == true) { - content = `1. What did I do ${weekOrDay}?
    + console.log('allIncluded called with outputTarget:', outputTarget); + console.log('Current window context:', window.location.href); + let scrumBody = null; + let scrumSubject = null; + let startingDate = ''; + let endingDate = ''; + let githubUsername = ''; + let gitlabUsername = ''; + let projectName = ''; + let lastWeekArray = []; + let nextWeekArray = []; + let reviewedPrsArray = []; + let githubIssuesData = null; + let lastWeekContribution = false; + let yesterdayContribution = false; + let githubPrsReviewData = null; + let githubUserData = null; + let githubPrsReviewDataProcessed = {}; + let showOpenLabel = true; + let showClosedLabel = true; + let userReason = ''; + let cacheInputValue = 10; // Default cache TTL value + + // GitLab data variables + let gitlabIssuesData = null; + let gitlabMergeRequestsData = null; + let gitlabCommentsData = null; + let gitlabUserData = null; + let gitlabPrsReviewDataProcessed = {}; + let gitlabProjects = []; + let gitlabProjectNameCache = {}; + + let pr_merged_button = + '
    closed
    '; + let pr_unmerged_button = + '
    open
    '; + + let issue_closed_button = + '
    closed
    '; + let issue_opened_button = + '
    open
    '; + + // GitHub cache object + let githubCache = { + data: null, + cacheKey: null, + timestamp: 0, + ttl: 10 * 60 * 1000, // cache valid for 10 mins + fetching: false, + queue: [], + errors: {}, + errorTTL: 60 * 1000, // 1 min error cache + subject: null, + }; + + // GitLab cache object + let gitlabCache = { + data: null, + cacheKey: null, + timestamp: 0, + ttl: 10 * 60 * 1000, // cache valid for 10 mins + fetching: false, + queue: [], + errors: {}, + errorTTL: 60 * 1000, // 1 min error cache + subject: null, + }; + + function getChromeData() { + console.log("Getting Chrome data for context:", outputTarget); + chrome.storage.local.get( + [ + 'githubUsername', + 'gitlabUsername', + 'selectedPlatform', + 'projectName', + 'enableToggle', + 'startingDate', + 'endingDate', + 'showOpenLabel', + 'showClosedLabel', + 'lastWeekContribution', + 'yesterdayContribution', + 'userReason', + 'githubCache', + 'gitlabCache', + 'cacheInputValue' + ], + (items) => { + console.log("Storage items received:", items); + + if (items.lastWeekContribution) { + lastWeekContribution = true; + handleLastWeekContributionChange(); + } + if (items.yesterdayContribution) { + yesterdayContribution = true; + handleYesterdayContributionChange(); + } + if (!items.enableToggle) { + enableToggle = items.enableToggle; + } + if (items.endingDate && !lastWeekContribution) { + endingDate = items.endingDate; + } + if (items.startingDate && !lastWeekContribution) { + startingDate = items.startingDate; + } + if (items.endingDate && !yesterdayContribution) { + endingDate = items.endingDate; + } + if (items.startingDate && !yesterdayContribution) { + startingDate = items.startingDate; + } + + // Handle platform-specific data fetching + const selectedPlatform = items.selectedPlatform || 'github'; + + if (selectedPlatform === 'github' && items.githubUsername) { + githubUsername = items.githubUsername; + console.log("About to fetch GitHub data for:", githubUsername); + fetchGithubData(); + } else if (selectedPlatform === 'gitlab' && items.gitlabUsername) { + gitlabUsername = items.gitlabUsername; + console.log("About to fetch GitLab data for:", gitlabUsername); + fetchGitLabData(); + } else { + if (outputTarget === 'popup') { + console.log("No username found - popup context"); + // Show error in popup + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + const platformName = selectedPlatform === 'github' ? 'GitHub' : 'GitLab'; + if (typeof Materialize !== 'undefined') { + Materialize.toast(`Please enter your ${platformName} username`, 3000); + } else { + console.warn(`Please enter your ${platformName} username`); + } + } else { + console.log("No username found - email context"); + console.warn(`No ${selectedPlatform} username found in storage`); + } + } + if (items.projectName) { + projectName = items.projectName; + } + if (items.cacheInputValue) { + cacheInputValue = items.cacheInputValue; + } + if (!items.showOpenLabel) { + showOpenLabel = false; + pr_unmerged_button = ''; + issue_opened_button = ''; + } + if (!items.showClosedLabel) { + showClosedLabel = false; + pr_merged_button = ''; + issue_closed_button = ''; + } + if (items.userReason) { + userReason = items.userReason; + } + if (!items.userReason) { + userReason = 'No Blocker at the moment'; + } + if (items.githubCache) { + githubCache.data = items.githubCache.data; + githubCache.cacheKey = items.githubCache.cacheKey; + githubCache.timestamp = items.githubCache.timestamp; + log('Restored GitHub cache from storage'); + } + if (items.gitlabCache) { + gitlabCache.data = items.gitlabCache.data; + gitlabCache.cacheKey = items.gitlabCache.cacheKey; + gitlabCache.timestamp = items.gitlabCache.timestamp; + log('Restored GitLab cache from storage'); + } + }, + ); + } + + function handleLastWeekContributionChange() { + endingDate = getToday(); + startingDate = getLastWeek(); + } + function handleYesterdayContributionChange() { + endingDate = getToday(); + startingDate = getYesterday(); + } + function getLastWeek() { + let today = new Date(); + let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + let lastWeekMonth = lastWeek.getMonth() + 1; + let lastWeekDay = lastWeek.getDate(); + let lastWeekYear = lastWeek.getFullYear(); + let lastWeekDisplayPadded = + ('0000' + lastWeekYear.toString()).slice(-4) + + '-' + + ('00' + lastWeekMonth.toString()).slice(-2) + + '-' + + ('00' + lastWeekDay.toString()).slice(-2); + return lastWeekDisplayPadded; + } + function getYesterday() { + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + let yesterdayMonth = yesterday.getMonth() + 1; + let yesterdayDay = yesterday.getDate(); + let yesterdayYear = yesterday.getFullYear(); + let yesterdayPadded = + ('0000' + yesterdayYear.toString()).slice(-4) + + '-' + + ('00' + yesterdayMonth.toString()).slice(-2) + + '-' + + ('00' + yesterdayDay.toString()).slice(-2); + return yesterdayPadded; + } + function getToday() { + let today = new Date(); + let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + let WeekMonth = Week.getMonth() + 1; + let WeekDay = Week.getDate(); + let WeekYear = Week.getFullYear(); + let WeekDisplayPadded = + ('0000' + WeekYear.toString()).slice(-4) + + '-' + + ('00' + WeekMonth.toString()).slice(-2) + + '-' + + ('00' + WeekDay.toString()).slice(-2); + return WeekDisplayPadded; + } + + getChromeData(); + + async function getCacheTTL() { + return new Promise((resolve) => { + chrome.storage.local.get(['cacheInputValue'], function (result) { + const ttlMinutes = result.cacheInputValue || 10; + resolve(ttlMinutes * 60 * 1000); + }); + }); + } + + function saveToStorage(data, subject = null) { + const cacheData = { + data: data, + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + subject: subject, + } + log(`Saving data to storage:`, { + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + hasSubject: !!subject, + }); + + return new Promise((resolve) => { + chrome.storage.local.set({ githubCache: cacheData }, () => { + if (chrome.runtime.lastError) { + logError('Storage save failed: ', chrome.runtime.lastError); + resolve(false); + } else { + log('Cache saved successfully'); + githubCache.data = data; + githubCache.subject = subject; + resolve(true); + } + }); + }); + } + + function saveGitLabToStorage(data, subject = null) { + const cacheData = { + data: data, + cacheKey: gitlabCache.cacheKey, + timestamp: gitlabCache.timestamp, + subject: subject, + } + log(`Saving GitLab data to storage:`, { + cacheKey: gitlabCache.cacheKey, + timestamp: gitlabCache.timestamp, + hasSubject: !!subject, + }); + + return new Promise((resolve) => { + chrome.storage.local.set({ gitlabCache: cacheData }, () => { + if (chrome.runtime.lastError) { + logError('GitLab storage save failed: ', chrome.runtime.lastError); + resolve(false); + } else { + log('GitLab cache saved successfully'); + gitlabCache.data = data; + gitlabCache.subject = subject; + resolve(true); + } + }); + }); + } + + function loadFromStorage() { + return new Promise((resolve) => { + chrome.storage.local.get(['githubCache', 'gitlabCache'], (items) => { + if (items.githubCache) { + githubCache.data = items.githubCache.data; + githubCache.cacheKey = items.githubCache.cacheKey; + githubCache.timestamp = items.githubCache.timestamp; + githubCache.subject = items.githubCache.subject; + log('Restored GitHub cache from storage'); + } + if (items.gitlabCache) { + gitlabCache.data = items.gitlabCache.data; + gitlabCache.cacheKey = items.gitlabCache.cacheKey; + gitlabCache.timestamp = items.gitlabCache.timestamp; + gitlabCache.subject = items.gitlabCache.subject; + log('Restored GitLab cache from storage'); + } + resolve(); + }); + }); + } + + async function fetchGithubData() { + const cacheKey = `${githubUsername}-${startingDate}-${endingDate}`; + + if (githubCache.fetching || (githubCache.cacheKey === cacheKey && githubCache.data)) { + log('Fetch already in progress or data already fetched. Skipping fetch.'); + return; + } + + log('Fetching Github data:', { + username: githubUsername, + startDate: startingDate, + endDate: endingDate, + }); + + log('CacheKey in cache:', githubCache.cacheKey); + log('Incoming cacheKey:', cacheKey); + log('Has data:', !!githubCache.data); + + // Check if we need to load from storage + if (!githubCache.data && !githubCache.fetching) { + await loadFromStorage(); + }; + + const currentTTL = await getCacheTTL(); + githubCache.ttl = currentTTL; + log(`Caching for ${currentTTL / (60 * 1000)} minutes`); + + const now = Date.now(); + const isCacheFresh = (now - githubCache.timestamp) < githubCache.ttl; + const isCacheKeyMatch = githubCache.cacheKey === cacheKey; + + if (githubCache.data && isCacheFresh && isCacheKeyMatch) { + log('Using cached data - cache is fresh and key matches'); + processGithubData(githubCache.data); + return Promise.resolve(); + } + // if cache key does not match our cache is stale, fetch new data + if (!isCacheKeyMatch) { + log('Cache key mismatch - fetching new Data'); + githubCache.data = null; + } else if (!isCacheFresh) { + log('Cache is stale - fetching new data'); + } + + // if fetching is in progress, queue the calls and return a promise resolved when done + if (githubCache.fetching) { + log('Fetch in progress, queuing requests'); + return new Promise((resolve, reject) => { + githubCache.queue.push({ resolve, reject }); + }); + } + + githubCache.fetching = true; + githubCache.cacheKey = cacheKey; + + let issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}+org%3Afossasia+created%3A${startingDate}..${endingDate}&per_page=100`; + let prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}+org%3Afossasia+updated%3A${startingDate}..${endingDate}&per_page=100`; + let userUrl = `https://api.github.com/users/${githubUsername}`; + + try { + // throttling 500ms to avoid burst + await new Promise(res => setTimeout(res, 500)); + + const [issuesRes, prRes, userRes] = await Promise.all([ + fetch(issueUrl), + fetch(prUrl), + fetch(userUrl), + ]); + + if (!issuesRes.ok) throw new Error(`Error fetching Github issues: ${issuesRes.status} ${issuesRes.statusText}`); + if (!prRes.ok) throw new Error(`Error fetching Github PR review data: ${prRes.status} ${prRes.statusText}`); + if (!userRes.ok) throw new Error(`Error fetching Github userdata: ${userRes.status} ${userRes.statusText}`); + + githubIssuesData = await issuesRes.json(); + githubPrsReviewData = await prRes.json(); + githubUserData = await userRes.json(); + + // Cache the data + githubCache.data = { githubIssuesData, githubPrsReviewData, githubUserData }; + githubCache.timestamp = Date.now(); + + await saveToStorage(githubCache.data); + processGithubData(githubCache.data); + + // Resolve queued calls + githubCache.queue.forEach(({ resolve }) => resolve()); + githubCache.queue = []; + } catch (err) { + logError('Fetch Failed:', err); + // Reject queued calls on error + githubCache.queue.forEach(({ reject }) => reject(err)); + githubCache.queue = []; + githubCache.fetching = false; + throw err; + } finally { + githubCache.fetching = false; + } + } + + async function fetchGitLabData() { + const cacheKey = `${gitlabUsername}-${startingDate}-${endingDate}`; + + if (gitlabCache.fetching || (gitlabCache.cacheKey === cacheKey && gitlabCache.data)) { + log('GitLab fetch already in progress or data already fetched. Skipping fetch.'); + return; + } + + log('Fetching GitLab data:', { + username: gitlabUsername, + startDate: startingDate, + endDate: endingDate, + }); + + log('GitLab CacheKey in cache:', gitlabCache.cacheKey); + log('Incoming GitLab cacheKey:', cacheKey); + log('Has GitLab data:', !!gitlabCache.data); + + // Check if we need to load from storage + if (!gitlabCache.data && !gitlabCache.fetching) { + await loadFromStorage(); + }; + + const currentTTL = await getCacheTTL(); + gitlabCache.ttl = currentTTL; + log(`GitLab caching for ${currentTTL / (60 * 1000)} minutes`); + + const now = Date.now(); + const isCacheFresh = (now - gitlabCache.timestamp) < gitlabCache.ttl; + const isCacheKeyMatch = gitlabCache.cacheKey === cacheKey; + + if (gitlabCache.data && isCacheFresh && isCacheKeyMatch) { + log('Using cached GitLab data - cache is fresh and key matches'); + processGitLabData(gitlabCache.data); + return Promise.resolve(); + } + // if cache key does not match our cache is stale, fetch new data + if (!isCacheKeyMatch) { + log('GitLab cache key mismatch - fetching new Data'); + gitlabCache.data = null; + } else if (!isCacheFresh) { + log('GitLab cache is stale - fetching new data'); + } + + // if fetching is in progress, queue the calls and return a promise resolved when done + if (gitlabCache.fetching) { + log('GitLab fetch in progress, queuing requests'); + return new Promise((resolve, reject) => { + gitlabCache.queue.push({ resolve, reject }); + }); + } + + gitlabCache.fetching = true; + gitlabCache.cacheKey = cacheKey; + + try { + // Use the GitLabHelper class to fetch data + const gitlabHelper = new GitLabHelper(); + const gitlabData = await gitlabHelper.fetchGitLabData(gitlabUsername, startingDate, endingDate); + + // Cache the data + gitlabCache.data = gitlabData; + gitlabCache.timestamp = Date.now(); + + await saveGitLabToStorage(gitlabCache.data); + processGitLabData(gitlabCache.data); + + // Resolve queued calls + gitlabCache.queue.forEach(({ resolve }) => resolve()); + gitlabCache.queue = []; + } catch (err) { + logError('GitLab Fetch Failed:', err); + // Reject queued calls on error + gitlabCache.queue.forEach(({ reject }) => reject(err)); + gitlabCache.queue = []; + gitlabCache.fetching = false; + throw err; + } finally { + gitlabCache.fetching = false; + } + } + + async function verifyCacheStatus() { + log('Cache Status: ', { + hasCachedData: !!githubCache.data, + cacheAge: githubCache.timestamp ? `${((Date.now() - githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : `no cache`, + cacheKey: githubCache.cacheKey, + isFetching: githubCache.fetching, + queueLength: githubCache.queue.length + }); + const storageData = await new Promise(resolve => { + chrome.storage.local.get('githubCache', resolve); + }); + log('Storage Status:', { + hasStoredData: !!storageData.githubCache, + storedCacheKey: storageData.githubCache?.cacheKey, + storageAge: storageData.githubCache?.timestamp ? + `${((Date.now() - storageData.githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : + 'no data' + }); + } + verifyCacheStatus(); + + function processGithubData(data) { + log('Processing Github data'); + githubIssuesData = data.githubIssuesData; + githubPrsReviewData = data.githubPrsReviewData; + githubUserData = data.githubUserData; + + log('GitHub data set:', { + issues: githubIssuesData?.items?.length || 0, + prs: githubPrsReviewData?.items?.length || 0, + user: githubUserData?.login + }); + + lastWeekArray = []; + nextWeekArray = []; + reviewedPrsArray = []; + githubPrsReviewDataProcessed = {}; + + // Update subject + if (!githubCache.subject && scrumSubject) { + scrumSubjectLoaded(); + } + } + + function processGitLabData(data) { + log('Processing GitLab data'); + gitlabIssuesData = data.issues; + gitlabMergeRequestsData = data.mergeRequests; + gitlabCommentsData = data.comments; + gitlabUserData = data.user; + gitlabProjects = data.projects || []; + + log('GitLab data set:', { + issues: gitlabIssuesData?.length || 0, + mergeRequests: gitlabMergeRequestsData?.length || 0, + comments: gitlabCommentsData?.length || 0, + user: gitlabUserData?.username + }); + + lastWeekArray = []; + nextWeekArray = []; + reviewedPrsArray = []; + gitlabPrsReviewDataProcessed = {}; + + // Update subject + if (!gitlabCache.subject && scrumSubject) { + scrumSubjectLoaded(); + } + } + + function formatDate(dateString) { + const date = new Date(dateString); + const options = { day: '2-digit', month: 'short', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); + } + + //load initial text in scrum body + function writeScrumBody() { + if (!enableToggle || (outputTarget === 'email' && hasInjectedContent)) return; + + if (outputTarget === 'email') { + if (!window.emailClientAdapter) { + console.error('Email client adapter not found'); + return; + } + if (!window.emailClientAdapter.isNewConversation()) { + console.log('Not a new conversation, skipping scrum helper'); + return; + } + } + + setTimeout(() => { + // Generate content first + let lastWeekUl = '
      '; + let i; + for (i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; + for (i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; + lastWeekUl += '
    '; + + let nextWeekUl = '
      '; + for (i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; + nextWeekUl += '
    '; + + let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); + let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + + // Create the complete content + let content; + if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } else { - content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    + } else { + content = `1. What did I do during the period?
    ${lastWeekUl}
    -2. What do I plan to do ${weekOrDay2}?
    +2. What do I plan to do next?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } - - if (outputTarget === 'popup') { - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - console.log("found div, updating content"); - scrumReport.innerHTML = content; - - // Reset generate button - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - } - } else { - - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.body) { - console.error('Email client editor not found'); - return; - } - window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); - } - }); - } - - function getProject() { - if (projectName != '') return projectName; - - var project = ''; - var url = window.location.href; - var projectUrl = url.substr(url.lastIndexOf('/') + 1); - if (projectUrl === 'susiai') project = 'SUSI.AI'; - else if (projectUrl === 'open-event') project = 'Open Event'; - return project; - } - - function scrumSubjectLoaded() { - try { - - - if (!enableToggle) return; - if (!scrumSubject) { - console.error('Subject element not found'); - return; - } - setTimeout(() => { - var name = githubUserData.name || githubUsername; - var project = getProject(); - var curDate = new Date(); - var year = curDate.getFullYear().toString(); - var date = curDate.getDate(); - var month = curDate.getMonth(); - month++; - if (month < 10) month = '0' + month; - if (date < 10) date = '0' + date; - var dateCode = year.toString() + month.toString() + date.toString(); - scrumSubject.value = '[Scrum] ' + name + ' - ' + project + ' - ' + dateCode + ' - False'; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - }); - } catch (err) { - console.err('Error while setting subject: ', err); - } - } - - function writeGithubPrsReviews() { - var items = githubPrsReviewData.items; - - reviewedPrsArray = []; - githubPrsReviewDataProcessed = {}; - - for (var i = 0; i < items.length; i++) { - var item = items[i]; - console.log(`Review item ${i + 1}/${items.length}:`, { - number: item.number, - author: item.user.login, - type: item.pull_request ? "PR" : "Issue", - state: item.state, - title: item.title - }); - - if (item.user.login === githubUsername) { - continue; - } - - var repository_url = item.repository_url; - var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - var title = item.title; - var number = item.number; - var html_url = item.html_url; - - if (!githubPrsReviewDataProcessed[project]) { - githubPrsReviewDataProcessed[project] = []; - } - - var obj = { - number: number, - html_url: html_url, - title: title, - state: item.state, - }; - githubPrsReviewDataProcessed[project].push(obj); - } - - for (var repo in githubPrsReviewDataProcessed) { - var repoLi = '
  • (' + repo + ') - Reviewed '; - if (githubPrsReviewDataProcessed[repo].length > 1) { - repoLi += 'PRs - '; - } else { - repoLi += 'PR - '; - } - if (githubPrsReviewDataProcessed[repo].length <= 1) { - for (var pr in githubPrsReviewDataProcessed[repo]) { - var pr_arr = githubPrsReviewDataProcessed[repo][pr]; - var prText = ''; - prText += `#${pr_arr.number} (${pr_arr.title}) `; - if (pr_arr.state === 'open') { - prText += issue_opened_button; - } else { - prText += issue_closed_button; - } - prText += '  '; - repoLi += prText; - } - } else { - repoLi += '
      '; - for (var pr1 in githubPrsReviewDataProcessed[repo]) { - var pr_arr1 = githubPrsReviewDataProcessed[repo][pr1]; - var prText1 = ''; - prText1 += `
    • #${pr_arr1.number} (${pr_arr1.title}) `; - if (pr_arr1.state === 'open') { - prText1 += issue_opened_button; - } else { - prText1 += issue_closed_button; - } - prText1 += '  
    • '; - repoLi += prText1; - } - repoLi += '
    '; - } - repoLi += '
  • '; - reviewedPrsArray.push(repoLi); - } - - writeScrumBody(); - } - function writeGithubIssuesPrs() { - var data = githubIssuesData; - var items = data.items; - - lastWeekArray = []; - nextWeekArray = []; - - for (var i = 0; i < items.length; i++) { - var item = items[i]; - var html_url = item.html_url; - var repository_url = item.repository_url; - var project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - var title = item.title; - var number = item.number; - var li = ''; - - if (item.pull_request) { - if (item.state === 'closed') { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_merged_button}
  • `; - } else if (item.state === 'open') { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_unmerged_button}
  • `; - } - } else { - if (item.state === 'open' && item.body && item.body.toUpperCase().indexOf('YES') > 0) { - var li2 = `
  • (${project}) - Work on Issue(#${number}) - ${title} ${issue_opened_button}
  • `; - nextWeekArray.push(li2); - } - if (item.state === 'open') { - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; - } else if (item.state === 'closed') { - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; - } - } - if (li) { - lastWeekArray.push(li); - } else { - } - } - writeScrumBody(); - } - var intervalBody = setInterval(() => { - if (!window.emailClientAdapter) return; - - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.body) return; - - clearInterval(intervalBody); - scrumBody = elements.body; - writeScrumBody(); - }, 500); - - var intervalSubject = setInterval(() => { - if (!githubUserData || !window.emailClientAdapter) return; - - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.subject) return; - - if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { - console.log('Not a new conversation, skipping subject interval'); - clearInterval(intervalSubject); - return; - } - - clearInterval(intervalSubject); - scrumSubject = elements.subject; - - setTimeout(() => { - scrumSubjectLoaded(); - }, 500); - }, 500); - - //check for github safe writing for both issues/prs and pr reviews - var intervalWriteGithub = setInterval(() => { - if (scrumBody && githubUsername && githubIssuesData && githubPrsReviewData) { - clearInterval(intervalWriteGithub); - writeGithubIssuesPrs(); - writeGithubPrsReviews(); - } - }, 500); - - // Helper functions for ISO date range - function getStartOfDayISO(dateString) { - return dateString + 'T00:00:00Z'; - } - function getEndOfDayISO(dateString) { - return dateString + 'T23:59:59Z'; - } - - function fetchGitlabData() { - // First get user's activities directly - var issuesUrl = `https://gitlab.com/api/v4/issues?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100&scope=all`; - console.log('[GitLab] Fetching all issues:', issuesUrl); - - // Fetch all issues created by the user - $.ajax({ - dataType: 'json', - type: 'GET', - url: issuesUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab issues:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - Materialize.toast('Error fetching GitLab issues. Please check your username.', 3000); - }, - success: (issues) => { - console.log('[GitLab] Issues fetched:', issues); - gitlabIssuesData = issues; - writeGitlabIssuesPrs(); - } - }); - - // Fetch all merge requests created by the user - var mrsUrl = `https://gitlab.com/api/v4/merge_requests?author_username=${gitlabUsername}&created_after=${getStartOfDayISO(startingDate)}&created_before=${getEndOfDayISO(endingDate)}&per_page=100&scope=all`; - console.log('[GitLab] Fetching all MRs:', mrsUrl); - - $.ajax({ - dataType: 'json', - type: 'GET', - url: mrsUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab MRs:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - Materialize.toast('Error fetching GitLab merge requests.', 3000); - }, - success: (mrs) => { - console.log('[GitLab] MRs fetched:', mrs); - gitlabPrsReviewData = mrs; - writeGitlabPrsReviews(); - } - }); - - // Fetch GitLab user data - var userUrl = `https://gitlab.com/api/v4/users?username=${gitlabUsername}`; - console.log('[GitLab] Fetching user data:', userUrl); - $.ajax({ - dataType: 'json', - type: 'GET', - url: userUrl, - error: (xhr, textStatus, errorThrown) => { - console.error('Error fetching GitLab user data:', { - status: xhr.status, - textStatus: textStatus, - error: errorThrown - }); - }, - success: (data) => { - if (data && data.length > 0) { - gitlabUserData = data[0]; - console.log('[GitLab] User data:', gitlabUserData); - } - }, - }); - } - - function getProjectName(item) { - // Get the project name from the full path - if (item.references?.full) { - return item.references.full.split('/').pop(); - } - if (item.project?.path_with_namespace) { - return item.project.path_with_namespace.split('/').pop(); - } - if (item.project?.name) { - return item.project.name; - } - return 'Unknown Project'; - } - - function writeGitlabIssuesPrs() { - if (!gitlabIssuesData) return; - - lastWeekArray = []; - nextWeekArray = []; - - for (var i = 0; i < gitlabIssuesData.length; i++) { - var item = gitlabIssuesData[i]; - var web_url = item.web_url; - var project = getProjectName(item); - var title = item.title; - var iid = item.iid; - var li = ''; - - if (item.state === 'closed') { - li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_closed_button}
  • `; - } else if (item.state === 'opened') { - li = `
  • (${project}) - Opened Issue(#${iid}) - ${title} ${issue_opened_button}
  • `; - } - - if (li) { - lastWeekArray.push(li); - } - } - console.log('[GitLab] Scrum lastWeekArray (issues):', lastWeekArray); - writeScrumBody(); - } - - function writeGitlabPrsReviews() { - if (!gitlabPrsReviewData) return; - - reviewedPrsArray = []; - gitlabPrsReviewDataProcessed = {}; - - for (var i = 0; i < gitlabPrsReviewData.length; i++) { - var item = gitlabPrsReviewData[i]; - var web_url = item.web_url; - var project = getProjectName(item); - var title = item.title; - var iid = item.iid; - - if (!gitlabPrsReviewDataProcessed[project]) { - gitlabPrsReviewDataProcessed[project] = []; - } - - var obj = { - number: iid, - html_url: web_url, - title: title, - state: item.state - }; - gitlabPrsReviewDataProcessed[project].push(obj); - } - - for (var repo in gitlabPrsReviewDataProcessed) { - var repoLi = '
  • (' + repo + ') - Created '; - if (gitlabPrsReviewDataProcessed[repo].length > 1) { - repoLi += 'MRs - '; - } else { - repoLi += 'MR - '; - } - if (gitlabPrsReviewDataProcessed[repo].length <= 1) { - for (var pr in gitlabPrsReviewDataProcessed[repo]) { - var pr_arr = gitlabPrsReviewDataProcessed[repo][pr]; - var prText = ''; - prText += `#${pr_arr.number} (${pr_arr.title}) `; - if (pr_arr.state === 'opened') { - prText += issue_opened_button; - } else { - prText += issue_closed_button; - } - prText += '  '; - repoLi += prText; - } - } else { - repoLi += '
      '; - for (var pr1 in gitlabPrsReviewDataProcessed[repo]) { - var pr_arr1 = gitlabPrsReviewDataProcessed[repo][pr1]; - var prText1 = ''; - prText1 += `
    • #${pr_arr1.number} (${pr_arr1.title}) `; - if (pr_arr1.state === 'opened') { - prText1 += issue_opened_button; - } else { - prText1 += issue_closed_button; - } - prText1 += '  
    • '; - repoLi += prText1; - } - repoLi += '
    '; - } - repoLi += '
  • '; - reviewedPrsArray.push(repoLi); - } - console.log('[GitLab] Scrum reviewedPrsArray (MRs):', reviewedPrsArray); - writeScrumBody(); - } + } + + if (outputTarget === 'popup') { + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = content; + } + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + } else if (outputTarget === 'email' && scrumBody) { + scrumBody.innerHTML = content; + hasInjectedContent = true; + } + }, 100); + } + + function scrumSubjectLoaded() { + try { + if (!enableToggle || hasInjectedContent) return; + if (!scrumSubject) { + console.error('Subject element not found'); + return; + } + setTimeout(() => { + const selectedPlatform = chrome.storage.local.get(['selectedPlatform'], (result) => { + const platform = result.selectedPlatform || 'github'; + let name, userData; + + if (platform === 'github') { + name = githubUserData?.name || githubUsername; + userData = githubUserData; + } else { + name = gitlabUserData?.name || gitlabUsername; + userData = gitlabUserData; + } + + let project = projectName || ''; + let curDate = new Date(); + let year = curDate.getFullYear().toString(); + let date = curDate.getDate(); + let month = curDate.getMonth(); + month++; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + let dateCode = year.toString() + month.toString() + date.toString(); + + const subject = `[Scrum] ${name} - ${project} - ${dateCode} - False`; + log('Generated subject:', subject); + + if (platform === 'github') { + githubCache.subject = subject; + saveToStorage(githubCache.data, subject); + } else { + gitlabCache.subject = subject; + saveGitLabToStorage(gitlabCache.data, subject); + } + + if (scrumSubject && scrumSubject.value !== subject) { + scrumSubject.value = subject; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + }); + } catch (err) { + console.err('Error while setting subject: ', err); + } + } + + function writeGithubPrsReviews() { + let items = githubPrsReviewData.items; + log('Processing PR reviews:', { + hasItems: !!items, + count: items?.length || 0 + }); + + if (!items || items.length === 0) { + log('No PR reviews found'); + return; + } + + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let html_url = item.html_url; + let repository_url = item.repository_url; + let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); + let title = item.title; + let number = item.number; + let li = ''; + + if (item.pull_request) { + if (item.state === 'closed') { + li = `
  • (${project}) - Reviewed PR (#${number}) - ${title} ${pr_merged_button}
  • `; + } else if (item.state === 'open') { + li = `
  • (${project}) - Reviewed PR (#${number}) - ${title} ${pr_unmerged_button}
  • `; + } + } + reviewedPrsArray.push(li); + } + writeScrumBody(); + } + + function writeGithubIssuesPrs() { + let items = githubIssuesData.items; + lastWeekArray = []; + nextWeekArray = []; + if (!items) { + logError('No Github issues data available'); + return; + } + for (let i = 0; i < items.length; i++) { + let item = items[i]; + let html_url = item.html_url; + let repository_url = item.repository_url; + let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); + let title = item.title; + let number = item.number; + let li = ''; + + if (item.pull_request) { + if (item.state === 'closed') { + li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_merged_button}
  • `; + } else if (item.state === 'open') { + li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_unmerged_button}
  • `; + } + } else { + // is a issue + if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { + //probably the author wants to work on this issue! + let li2 = + '
  • (' + + project + + ') - Work on Issue(#' + + number + + ") - " + + title + + ' ' + + issue_opened_button + + '  
  • '; + nextWeekArray.push(li2); + } + if (item.state === 'open') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; + } else if (item.state === 'closed') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + } else { + li = + '
  • (' + + project + + ') - Opened Issue(#' + + number + + ") - " + + title + + '
  • '; + } + } + lastWeekArray.push(li); + } + writeScrumBody(); + } + + function writeGitLabMergeRequests() { + let mergeRequests = gitlabMergeRequestsData; + log('Processing GitLab merge requests:', { + hasItems: !!mergeRequests, + count: mergeRequests?.length || 0 + }); + + if (!mergeRequests || mergeRequests.length === 0) { + log('No GitLab merge requests found'); + return; + } + + for (let i = 0; i < mergeRequests.length; i++) { + let mr = mergeRequests[i]; + let project = getGitLabProjectName(mr.project_id); + let title = mr.title || 'Untitled MR'; + let number = mr.iid || mr.id || ''; + let mrUrl = mr.web_url || '#'; + let mrState = mr.state || 'unknown'; + let li = ''; + if (mr.author && gitlabUsername && mr.author.username === gitlabUsername) { + // User created this MR + if (mrState === 'merged') { + li = `
  • (${project}) - Made MR (#${number}) - ${title} ${pr_merged_button}
  • `; + } else if (mrState === 'opened') { + li = `
  • (${project}) - Made MR (#${number}) - ${title} ${pr_unmerged_button}
  • `; + } + lastWeekArray.push(li); + } else { + // User reviewed this MR + if (mrState === 'merged') { + li = `
  • (${project}) - Reviewed MR (#${number}) - ${title} ${pr_merged_button}
  • `; + } else if (mrState === 'opened') { + li = `
  • (${project}) - Reviewed MR (#${number}) - ${title} ${pr_unmerged_button}
  • `; + } + reviewedPrsArray.push(li); + } + } + writeScrumBody(); + } + + function writeGitLabIssues() { + let issues = gitlabIssuesData; + log('Processing GitLab issues:', { + hasItems: !!issues, + count: issues?.length || 0 + }); + + if (!issues || issues.length === 0) { + log('No GitLab issues found'); + return; + } + + for (let i = 0; i < issues.length; i++) { + let issue = issues[i]; + let project = getGitLabProjectName(issue.project_id); + let title = issue.title || 'Untitled Issue'; + let number = issue.iid || issue.id || ''; + let issueUrl = issue.web_url || '#'; + let issueState = issue.state || 'unknown'; + let li = ''; + if (issueState === 'opened') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; + } else if (issueState === 'closed') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + } else { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; + } + lastWeekArray.push(li); + } + writeScrumBody(); + } + + function writeGitLabComments() { + // Comments are not being fetched for GitLab, so this function is empty + // This prevents errors when the interval tries to call this function + return; + } + + let intervalBody = setInterval(() => { + if (!window.emailClientAdapter) return; + + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.body) return; + + clearInterval(intervalBody); + scrumBody = elements.body; + writeScrumBody(); + }, 500); + + let intervalSubject = setInterval(() => { + const selectedPlatform = chrome.storage.local.get(['selectedPlatform'], (result) => { + const platform = result.selectedPlatform || 'github'; + let userData; + + if (platform === 'github') { + userData = githubUserData; + } else { + userData = gitlabUserData; + } + + if (!userData || !window.emailClientAdapter) return; + + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.subject) return; + + if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { + console.log('Not a new conversation, skipping subject interval'); + clearInterval(intervalSubject); + return; + } + + clearInterval(intervalSubject); + scrumSubject = elements.subject; + + setTimeout(() => { + scrumSubjectLoaded(); + }, 500); + }); + }, 500); + + //check for github safe writing + let intervalWriteGithubIssues = setInterval(() => { + if (outputTarget === 'popup') { + if (githubUsername && githubIssuesData) { + clearInterval(intervalWriteGithubIssues); + writeGithubIssuesPrs(); + } + } else { + if (scrumBody && githubUsername && githubIssuesData) { + clearInterval(intervalWriteGithubIssues); + writeGithubIssuesPrs(); + } + } + }, 500); + let intervalWriteGithubPrs = setInterval(() => { + if (outputTarget === 'popup') { + if (githubUsername && githubPrsReviewData) { + clearInterval(intervalWriteGithubPrs); + writeGithubPrsReviews(); + } + } else { + if (scrumBody && githubUsername && githubPrsReviewData) { + clearInterval(intervalWriteGithubPrs); + writeGithubPrsReviews(); + } + } + }, 500); + + //check for gitlab safe writing + let intervalWriteGitLabIssues = setInterval(() => { + if (outputTarget === 'popup') { + if (gitlabUsername && gitlabIssuesData) { + clearInterval(intervalWriteGitLabIssues); + writeGitLabIssues(); + } + } else { + if (scrumBody && gitlabUsername && gitlabIssuesData) { + clearInterval(intervalWriteGitLabIssues); + writeGitLabIssues(); + } + } + }, 500); + + let intervalWriteGitLabMergeRequests = setInterval(() => { + if (outputTarget === 'popup') { + if (gitlabUsername && gitlabMergeRequestsData) { + clearInterval(intervalWriteGitLabMergeRequests); + writeGitLabMergeRequests(); + } + } else { + if (scrumBody && gitlabUsername && gitlabMergeRequestsData) { + clearInterval(intervalWriteGitLabMergeRequests); + writeGitLabMergeRequests(); + } + } + }, 500); + + let intervalWriteGitLabComments = setInterval(() => { + // Comments are not being fetched for GitLab, so this interval is disabled + clearInterval(intervalWriteGitLabComments); + }, 500); + + if (!refreshButton_Placed) { + let intervalWriteButton = setInterval(() => { + if (document.getElementsByClassName('F0XO1GC-x-b').length == 3 && scrumBody && enableToggle) { + refreshButton_Placed = true; + clearInterval(intervalWriteButton); + let td = document.createElement('td'); + let button = document.createElement('button'); + button.style = 'background-image:none;background-color:#3F51B5;'; + button.setAttribute('class', 'F0XO1GC-n-a F0XO1GC-G-a'); + button.title = 'Rewrite your SCRUM using updated settings!'; + button.id = 'refreshButton'; + let elemText = document.createTextNode('↻ Rewrite SCRUM!'); + button.appendChild(elemText); + td.appendChild(button); + document.getElementsByClassName('F0XO1GC-x-b')[0].children[0].children[0].appendChild(td); + document.getElementById('refreshButton').addEventListener('click', handleRefresh); + } + }, 1000); + } + function handleRefresh() { + hasInjectedContent = false; // Reset the flag before refresh + allIncluded(); + } + + function getGitLabProjectName(projectId) { + if (!projectId) return 'Unknown Project'; + if (!gitlabProjects || !Array.isArray(gitlabProjects)) gitlabProjects = []; + // 1. Try local projects array + const project = gitlabProjects.find(p => p.id === projectId); + if (project) return project.name; + // 2. Try cache + if (gitlabProjectNameCache[projectId]) return gitlabProjectNameCache[projectId]; + // 3. Fetch from GitLab API and cache + const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}`; + fetch(apiUrl) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data && data.name) { + gitlabProjectNameCache[projectId] = data.name; + // Update all displayed project names in the DOM if needed + document.querySelectorAll(`li[data-gitlab-project-id='${projectId}'] i`).forEach(el => { + el.textContent = `(${data.name})`; + }); + } + }); + return 'Loading...'; + } } + +// Only call allIncluded for email contexts, not popup contexts allIncluded('email'); + $('button>span:contains(New conversation)').parent('button').click(() => { - allIncluded(); + allIncluded(); }); window.generateScrumReport = function () { - allIncluded('popup'); -}; \ No newline at end of file + allIncluded('popup'); +} + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'forceRefresh') { + forceGithubDataRefresh() + .then(result => sendResponse(result)).catch(err => { + console.error('Force refresh failed:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; + } +}); + +function log(...args) { + console.log('[ScrumHelper]', ...args); +} + +function logError(...args) { + console.error('[ScrumHelper]', ...args); +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..bfdd702 --- /dev/null +++ b/test.html @@ -0,0 +1,203 @@ + + + + + + + Scrum Helper GitLab Test + + + + +

    Scrum Helper GitLab Integration Test

    + +
    +

    Test Instructions

    +

    This page tests the GitLab integration for the Scrum Helper extension.

    +
      +
    1. Load the extension in your browser
    2. +
    3. Open the extension popup
    4. +
    5. Select GitLab platform
    6. +
    7. Enter a GitLab username
    8. +
    9. Click "Generate Report"
    10. +
    +
    + +
    +

    Manual Test

    +

    Test the GitLab integration manually:

    + + +
    +
    + +
    +

    Extension Status

    +

    Extension loaded: Checking...

    +

    GitLab Helper available: Checking...

    +
    + + + + + \ No newline at end of file From cc20f14a8b144f2ff2f587761d24f3f5d4378c7d Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 19 Jun 2025 13:27:38 +0530 Subject: [PATCH 06/40] removed the testing logs --- README.md | 83 +++++++++------ src/scripts/scrumHelper.js | 8 +- test.html | 203 ------------------------------------- 3 files changed, 54 insertions(+), 240 deletions(-) delete mode 100644 test.html diff --git a/README.md b/README.md index 75eafd9..33640c4 100644 --- a/README.md +++ b/README.md @@ -6,59 +6,73 @@ ## Features -- Fetches your GitHub PRs, Issues, and reviewed PRs -- Auto-generates scrum updates -- Supports Google Groups, Gmail, Yahoo, and Outlook compose windows +- Automatically fetches your Git activity, including: + - **GitHub**: issues, pull requests, and code reviews + - **GitLab**: issues, merge requests, and reviewed merge requests +- Supports both **GitHub** and **GitLab** platforms +- Simple platform selection UI (checkboxes for GitHub and GitLab) +- Generates editable scrum updates based on your selected date range +- Integrates directly with compose windows in Google Groups, Gmail, Yahoo Mail, and Outlook +- Standalone popup interface for previewing and copying scrum reports ## How to install +### For Chrome: + 1. Clone this repository to your local machine. 2. Go to `chrome://extensions` on your chrome browser. 3. Enable Developer Mode (toggle in the top-right) if not already. 4. Click Load unpacked and select the `src` folder inside the cloned repo 5. Click the Scrum Helper icon on your browser toolbar -6. Fill in your settings in the popup (GitHub username, date range, etc.) +6. Fill in your settings in the popup (GitHub or GitLab username, date range, etc.) + +### For Firefox: + +1. Clone this repository to your local machine. +2. Open Firefox and navigate to `about:debugging` +3. Click on "This Firefox" in the left sidebar +4. Click "Load Temporary Add-on..." +5. Navigate to the `src` folder inside the cloned repo and select the `manifest.json` file +6. The extension will be loaded temporarily and will remain active only for the current browser session +7. Click the Scrum Helper icon on your browser toolbar +8. Fill in your settings in the popup (GitHub or GitLab username, date range, etc.) + +**Note for Firefox users:** The extension will be automatically removed when you close Firefox. You'll need to reload it each time you start a new browser session by repeating steps 2-5. + +**Persistence Note:** If you need the extension to persist between sessions, use Firefox Developer Edition. You can enable persistence by setting `xpinstall.signatures.required` to `false` in the browser's configuration. ## Usage -### For Google Groups: +### Platform Selection +- In the popup, you can select either **GitHub** or **GitLab** using the checkboxes. Only one can be selected at a time. +- Enter your username for the selected platform. +- The extension will fetch and generate your scrum report from the chosen platform. +### For Google Groups: - Open Google Groups New Topic - Start a New Conversation - Refresh the page to apply the Scrum Helper settings - Use the pre-filled scrum and edit as needed ### For Gmail, Yahoo, and Outlook: - - Open the Compose window. -- Ensure the Scrum Helper settings are applied (follow step 6 above) +- Ensure the Scrum Helper settings are applied (see install steps above) - The extension will prefill scrum content for you to edit -### New Features -1. **Standalone Popup Interface** - - Generate reports directly from the extension popup - - Live preview of the report before sending - - Rich text formatting with clickable links - - Copy report to clipboard with proper formatting - -### Usage Standalone +### Standalone Popup Usage - Click on `GENERATE` button to generate the scrum preview. - Edit it in the window. - Copy the rich HTML using the `COPY` button. -## Setting up the code locally - -``` -$ git clone https://github.com/fossasia/scrum_helper/ -$ cd scrum_helper -$ npm install -``` +## What Data is Fetched? +- **GitHub:** Issues, pull requests, and code reviews (for the selected date range and organization) +- **GitLab:** Issues, merge requests, and reviewed merge requests (for the selected date range) ## Screenshots -![SCRUM](/docs/images/scrum.png) +![SCRUM](docs/images/scrum.png) -![POPUP](/docs/images/popup.png) +![POPUP](docs/images/popup.png) ![STANDALONE](docs/images/standalone.png) @@ -70,29 +84,32 @@ Scrum Helper is not limited to the [FOSSASIA](https://github.com/fossasia) organ 1. **Install the Extension** -- Load it into your browser through [Chrome Extension Developer Mode](https://developer.chrome.com/docs/extensions/mv3/getstarted/). +* For Chrome: Load it into your browser through [Chrome Extension Developer Mode](https://developer.chrome.com/docs/extensions/mv3/getstarted/). +* For Firefox: Load it as a temporary add-on through `about:debugging` as described above. 2. **Update the Organization** + * Currently, the extension uses `org:fossasia` to fetch GitHub issues and PRs. + * To make it work with your GitHub organization: + * Open `scrumHelper.js` (or wherever the GitHub API URLs are defined). + * Replace: - - Currently, the extension uses `org:fossasia` to fetch GitHub issues and PRs. - - To make it work with your GitHub organization: - - Open `scrumHelper.js` (or wherever the GitHub API URLs are defined). - - Replace: ```js +org:fossasia+ ``` + with: + ```js +org:your-org-name+ ``` + **Example** ![Code Snippet ]() 3. **Build the Extension** - - - Save your changes. - - Rebuild or reload the extension in your browser (`chrome://extensions` → Refresh your extension). - + * Save your changes. + * For Chrome: Rebuild or reload the extension in your browser (`chrome://extensions` → Refresh your extension). + * For Firefox: Reload the temporary add-on by going to `about:debugging` → "This Firefox" → Click "Reload" next to your extension. 4. **Get Customized SCRUM Reports** - The reports will now be generated using contributions from your organization. diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 374e676..999d17a 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -619,16 +619,16 @@ function allIncluded(outputTarget = 'email') { // Create the complete content let content; if (lastWeekContribution == true || yesterdayContribution == true) { - content = `1. What did I do ${weekOrDay}?
    + content = `1. What did I do ${weekOrDay}? ${lastWeekUl}
    -2. What do I plan to do ${weekOrDay2}?
    +2. What do I plan to do ${weekOrDay2}? ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; } else { - content = `1. What did I do during the period?
    + content = `1. What did I do during the period? ${lastWeekUl}
    -2. What do I plan to do next?
    +2. What do I plan to do next? ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; diff --git a/test.html b/test.html deleted file mode 100644 index bfdd702..0000000 --- a/test.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - Scrum Helper GitLab Test - - - - -

    Scrum Helper GitLab Integration Test

    - -
    -

    Test Instructions

    -

    This page tests the GitLab integration for the Scrum Helper extension.

    -
      -
    1. Load the extension in your browser
    2. -
    3. Open the extension popup
    4. -
    5. Select GitLab platform
    6. -
    7. Enter a GitLab username
    8. -
    9. Click "Generate Report"
    10. -
    -
    - -
    -

    Manual Test

    -

    Test the GitLab integration manually:

    - - -
    -
    - -
    -

    Extension Status

    -

    Extension loaded: Checking...

    -

    GitLab Helper available: Checking...

    -
    - - - - - \ No newline at end of file From f0dc29e99c4f68863ef0a4ec1ea1d9452caee5d9 Mon Sep 17 00:00:00 2001 From: Preeti Yadav Date: Thu, 19 Jun 2025 13:37:47 +0530 Subject: [PATCH 07/40] removed testing logs --- test.html | 203 ------------------------------------------------------ 1 file changed, 203 deletions(-) delete mode 100644 test.html diff --git a/test.html b/test.html deleted file mode 100644 index bfdd702..0000000 --- a/test.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - Scrum Helper GitLab Test - - - - -

    Scrum Helper GitLab Integration Test

    - -
    -

    Test Instructions

    -

    This page tests the GitLab integration for the Scrum Helper extension.

    -
      -
    1. Load the extension in your browser
    2. -
    3. Open the extension popup
    4. -
    5. Select GitLab platform
    6. -
    7. Enter a GitLab username
    8. -
    9. Click "Generate Report"
    10. -
    -
    - -
    -

    Manual Test

    -

    Test the GitLab integration manually:

    - - -
    -
    - -
    -

    Extension Status

    -

    Extension loaded: Checking...

    -

    GitLab Helper available: Checking...

    -
    - - - - - \ No newline at end of file From e7ef3f23a23f4af20556561890683801ede5ecd5 Mon Sep 17 00:00:00 2001 From: Preeti Yadav Date: Thu, 19 Jun 2025 16:20:07 +0530 Subject: [PATCH 08/40] Update scrumHelper.js --- src/scripts/scrumHelper.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index c8477d0..d5f4d02 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -621,13 +621,11 @@ function allIncluded(outputTarget = 'email') { let content; if (lastWeekContribution == true || yesterdayContribution == true) { content = `1. What did I do ${weekOrDay}?
    - - -${lastWeekUl}
    -2. What do I plan to do ${weekOrDay2}?
    -${nextWeekUl}
    -3. What is blocking me from making progress?
    -${userReason}`; + ${lastWeekUl}
    + 2. What do I plan to do ${weekOrDay2}?
    + ${nextWeekUl}
    + 3. What is blocking me from making progress?
    + ${userReason}`; } else { content = `1. What did I do during the period?
    ${lastWeekUl}
    From ee73d9a80d1ad34418c90213e3257f35d3db57f5 Mon Sep 17 00:00:00 2001 From: Preeti Yadav Date: Thu, 19 Jun 2025 16:37:58 +0530 Subject: [PATCH 09/40] Update scrumHelper.js --- src/scripts/scrumHelper.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index d5f4d02..f38373c 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -620,18 +620,18 @@ function allIncluded(outputTarget = 'email') { // Create the complete content let content; if (lastWeekContribution == true || yesterdayContribution == true) { - content = `1. What did I do ${weekOrDay}?
    - ${lastWeekUl}
    - 2. What do I plan to do ${weekOrDay2}?
    - ${nextWeekUl}
    - 3. What is blocking me from making progress?
    + content = `1. What did I do ${weekOrDay}? + ${lastWeekUl} + 2. What do I plan to do ${weekOrDay2}? + ${nextWeekUl} + 3. What is blocking me from making progress? ${userReason}`; } else { - content = `1. What did I do during the period?
    -${lastWeekUl}
    -2. What do I plan to do next?
    -${nextWeekUl}
    -3. What is blocking me from making progress?
    + content = `1. What did I do during the period? +${lastWeekUl} +2. What do I plan to do next? +${nextWeekUl} +3. What is blocking me from making progress? ${userReason}`; } From cc01b87a93ba2a457bd77d7d8c08c665d6137b07 Mon Sep 17 00:00:00 2001 From: Preeti Yadav Date: Thu, 19 Jun 2025 16:44:43 +0530 Subject: [PATCH 10/40] Update index.css --- src/index.css | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index 6c007ab..b291cf1 100644 --- a/src/index.css +++ b/src/index.css @@ -338,4 +338,20 @@ body,input,div,h3,h4,p,label,hr, #scrumReport{ .dark-mode .tooltip-container.tooltip-right .tooltip-bubble::after { border-color: transparent #374151 transparent transparent; -} \ No newline at end of file +} +#scrumReport b { + display: block; + margin-bottom: 4px; + margin-top: 16px; +} +#scrumReport b:first-child { + margin-top: 0; +} +#scrumReport ul { + margin-top: 0; + margin-bottom: 12px; + padding-left: 20px; +} +#scrumReport li { + margin-bottom: 2px; +} From 62e7a870ff232c211359017b18e59a81e4b5492a Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Tue, 8 Jul 2025 23:23:58 +0530 Subject: [PATCH 11/40] ui changes --- src/popup.html | 53 ++++++++- src/scripts/popup.js | 223 +++++++++++++++++++++++++++++++++++-- src/scripts/scrumHelper.js | 109 +++++++++++------- 3 files changed, 328 insertions(+), 57 deletions(-) diff --git a/src/popup.html b/src/popup.html index ee0b79d..67ae18f 100644 --- a/src/popup.html +++ b/src/popup.html @@ -1,4 +1,3 @@ - @@ -18,6 +17,26 @@ overflow-y: scroll; background: #eae4e4; } + + #platformSelect option { + font-family: 'FontAwesome', 'Arial', sans-serif; + } + + #customPlatformDropdown { + position: relative; + } + + #platformDropdownList { + display: none; + } + + #customPlatformDropdown.open #platformDropdownList { + display: block; + } + + #platformDropdownBtn:focus { + outline: 2px solid #2563eb; + } @@ -58,6 +77,28 @@

    Scrum
    +
    + +
    + + + +
    +

    Your Project Name @@ -65,13 +106,15 @@

    Your Project Name This is the name that appears in the subject line of your email, followed by timestamp.

    + +

    -

    Your Github Username

    - Your GitHub Username

    +
    @@ -275,9 +318,9 @@

    Note:

    + - - + \ No newline at end of file diff --git a/src/scripts/popup.js b/src/scripts/popup.js index b97d6bf..9b6d7e1 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -62,6 +62,10 @@ document.addEventListener('DOMContentLoaded', function () { const orgInput = document.getElementById('orgInput'); const setOrgBtn = document.getElementById('setOrgBtn'); + const platformSelect = document.getElementById('platformSelect'); + const usernameLabel = document.getElementById('usernameLabel'); + const platformUsername = document.getElementById('platformUsername'); + chrome.storage.local.get(['darkMode'], function (result) { if (result.darkMode) { body.classList.add('dark-mode'); @@ -188,8 +192,8 @@ document.addEventListener('DOMContentLoaded', function () { } } - chrome.storage.local.get(['enableToggle'], (items) => { - const enableToggle = items.enableToggle !== false; + chrome.storage.local.get(['enable'], (items) => { + const enableToggle = items.enable !== false; updateContentState(enableToggle); if (!enableToggle) { return; @@ -199,9 +203,9 @@ document.addEventListener('DOMContentLoaded', function () { }) chrome.storage.onChanged.addListener((changes, namespace) => { - if (namespace === 'local' && changes.enableToggle) { - updateContentState(changes.enableToggle.newValue); - if (changes.enableToggle.newValue) { + if (namespace === 'local' && changes.enable) { + updateContentState(changes.enable.newValue); + if (changes.enable.newValue) { // re-initialize if enabled initializePopup(); } @@ -209,19 +213,60 @@ document.addEventListener('DOMContentLoaded', function () { }); function initializePopup() { + // Restore all persistent fields immediately on DOMContentLoaded + const projectNameInput = document.getElementById('projectName'); + const orgInput = document.getElementById('orgInput'); + const userReasonInput = document.getElementById('userReason'); + const showOpenLabelCheckbox = document.getElementById('showOpenLabel'); + const showCommitsCheckbox = document.getElementById('showCommits'); + const githubTokenInput = document.getElementById('githubToken'); + const cacheInput = document.getElementById('cacheInput'); + const enableToggleSwitch = document.getElementById('enable'); + const lastWeekRadio = document.getElementById('lastWeekContribution'); + const yesterdayRadio = document.getElementById('yesterdayContribution'); + const startingDateInput = document.getElementById('startingDate'); + const endingDateInput = document.getElementById('endingDate'); + const platformUsername = document.getElementById('platformUsername'); + + chrome.storage.local.get([ + 'projectName', 'orgName', 'userReason', 'showOpenLabel', 'showCommits', 'githubToken', 'cacheInput', + 'enable', 'lastWeekContribution', 'yesterdayContribution', 'startingDate', 'endingDate', 'selectedTimeframe', 'platform', 'platformUsername' + ], function (result) { + if (result.projectName) projectNameInput.value = result.projectName; + if (result.orgName) orgInput.value = result.orgName; + if (result.userReason) userReasonInput.value = result.userReason; + if (typeof result.showOpenLabel !== 'undefined') showOpenLabelCheckbox.checked = result.showOpenLabel; + if (typeof result.showCommits !== 'undefined') showCommitsCheckbox.checked = result.showCommits; + if (result.githubToken) githubTokenInput.value = result.githubToken; + if (result.cacheInput) cacheInput.value = result.cacheInput; + if (typeof result.enable !== 'undefined' && enableToggleSwitch) enableToggleSwitch.checked = result.enable; + if (typeof result.lastWeekContribution !== 'undefined') lastWeekRadio.checked = result.lastWeekContribution; + if (typeof result.yesterdayContribution !== 'undefined') yesterdayRadio.checked = result.yesterdayContribution; + if (result.startingDate) startingDateInput.value = result.startingDate; + if (result.endingDate) endingDateInput.value = result.endingDate; + platformUsername.value = result.platformUsername || ''; + }); // Button setup const generateBtn = document.getElementById('generateReport'); const copyBtn = document.getElementById('copyReport'); generateBtn.addEventListener('click', function () { - // Check org input value before generating report - let org = orgInput.value.trim().toLowerCase(); - // Allow empty org to fetch all GitHub activities - chrome.storage.local.set({ orgName: org }, () => { - generateBtn.innerHTML = ' Generating...'; - generateBtn.disabled = true; - window.generateScrumReport(); + chrome.storage.local.set({ + platform: platformSelect.value, + platformUsername: platformUsername.value + }, () => { + let org = orgInput.value.trim().toLowerCase(); + chrome.storage.local.set({ orgName: org }, () => { + // Reload platform from storage before generating report + chrome.storage.local.get(['platform'], function (res) { + platformSelect.value = res.platform || 'github'; + updatePlatformUI(platformSelect.value); + generateBtn.innerHTML = ' Generating...'; + generateBtn.disabled = true; + window.generateScrumReport && window.generateScrumReport(); + }); + }); }); }); @@ -332,6 +377,60 @@ document.addEventListener('DOMContentLoaded', function () { }); } }); + + // Save all fields to storage on input/change + projectNameInput.addEventListener('input', function () { + chrome.storage.local.set({ projectName: projectNameInput.value }); + }); + orgInput.addEventListener('input', function () { + chrome.storage.local.set({ orgName: orgInput.value.trim().toLowerCase() }); + }); + userReasonInput.addEventListener('input', function () { + chrome.storage.local.set({ userReason: userReasonInput.value }); + }); + showOpenLabelCheckbox.addEventListener('change', function () { + chrome.storage.local.set({ showOpenLabel: showOpenLabelCheckbox.checked }); + }); + showCommitsCheckbox.addEventListener('change', function () { + chrome.storage.local.set({ showCommits: showCommitsCheckbox.checked }); + }); + githubTokenInput.addEventListener('input', function () { + chrome.storage.local.set({ githubToken: githubTokenInput.value }); + }); + cacheInput.addEventListener('input', function () { + chrome.storage.local.set({ cacheInput: cacheInput.value }); + }); + if (enableToggleSwitch) { + enableToggleSwitch.addEventListener('change', function () { + chrome.storage.local.set({ enable: enableToggleSwitch.checked }); + }); + } + lastWeekRadio.addEventListener('change', function () { + chrome.storage.local.set({ lastWeekContribution: lastWeekRadio.checked }); + }); + yesterdayRadio.addEventListener('change', function () { + chrome.storage.local.set({ yesterdayContribution: yesterdayRadio.checked }); + }); + startingDateInput.addEventListener('input', function () { + chrome.storage.local.set({ startingDate: startingDateInput.value }); + }); + endingDateInput.addEventListener('input', function () { + chrome.storage.local.set({ endingDate: endingDateInput.value }); + }); + + // Save username to storage on input + platformUsername.addEventListener('input', function () { + chrome.storage.local.set({ platformUsername: platformUsername.value }); + }); + + // Platform change: clear username, update label/placeholder, clear storage + platformSelect.addEventListener('change', function () { + const platform = platformSelect.value; + chrome.storage.local.set({ platform }); + updatePlatformUI(platform); + platformUsername.value = ''; + chrome.storage.local.set({ platformUsername: '' }); + }); } function showReportView() { @@ -522,6 +621,106 @@ document.addEventListener('DOMContentLoaded', function () { } + // Restore platform from storage or default to github + chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + platformSelect.value = platform; + updatePlatformUI(platform); + }); + + // Update UI for platform + function updatePlatformUI(platform) { + if (platform === 'gitlab') { + usernameLabel.textContent = 'Your GitLab Username'; + platformUsername.placeholder = 'Required for fetching data from GitLab'; + } else { + usernameLabel.textContent = 'Your GitHub Username'; + platformUsername.placeholder = 'Required for fetching data from GitHub'; + } + } + + // On platform change + platformSelect.addEventListener('change', function () { + const platform = platformSelect.value; + chrome.storage.local.set({ platform }); + updatePlatformUI(platform); + }); + + // Custom platform dropdown logic + const customDropdown = document.getElementById('customPlatformDropdown'); + const dropdownBtn = document.getElementById('platformDropdownBtn'); + const dropdownList = document.getElementById('platformDropdownList'); + const dropdownSelected = document.getElementById('platformDropdownSelected'); + const platformSelectHidden = document.getElementById('platformSelect'); + + function setPlatformDropdown(value) { + if (value === 'gitlab') { + dropdownSelected.innerHTML = ' GitLab'; + } else { + dropdownSelected.innerHTML = ' GitHub'; + } + platformSelectHidden.value = value; + chrome.storage.local.set({ platform: value }); + updatePlatformUI(value); + platformUsername.value = ''; + chrome.storage.local.set({ platformUsername: '' }); + } + + dropdownBtn.addEventListener('click', function (e) { + e.stopPropagation(); + customDropdown.classList.toggle('open'); + dropdownList.classList.toggle('hidden'); + }); + + dropdownList.querySelectorAll('li').forEach(item => { + item.addEventListener('click', function (e) { + setPlatformDropdown(this.getAttribute('data-value')); + customDropdown.classList.remove('open'); + dropdownList.classList.add('hidden'); + }); + }); + + document.addEventListener('click', function (e) { + if (!customDropdown.contains(e.target)) { + customDropdown.classList.remove('open'); + dropdownList.classList.add('hidden'); + } + }); + + // Keyboard navigation + platformDropdownBtn.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + customDropdown.classList.add('open'); + dropdownList.classList.remove('hidden'); + dropdownList.querySelector('li').focus(); + } + }); + dropdownList.querySelectorAll('li').forEach((item, idx, arr) => { + item.setAttribute('tabindex', '0'); + item.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + (arr[idx + 1] || arr[0]).focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + (arr[idx - 1] || arr[arr.length - 1]).focus(); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setPlatformDropdown(this.getAttribute('data-value')); + customDropdown.classList.remove('open'); + dropdownList.classList.add('hidden'); + dropdownBtn.focus(); + } + }); + }); + + // On load, restore platform from storage + chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + setPlatformDropdown(platform); + }); + }); // Tooltip bubble diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 7a9480b..c3ab8fe 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -1,4 +1,3 @@ - const DEBUG = true; function log(...args) { @@ -18,6 +17,9 @@ let hasInjectedContent = false; let scrumGenerationInProgress = false; let orgName = ''; +let platform = 'github'; +let platformUsername = ''; +let gitlabHelper = null; function allIncluded(outputTarget = 'email') { if (scrumGenerationInProgress) { @@ -66,9 +68,11 @@ function allIncluded(outputTarget = 'email') { // let linkStyle = ''; function getChromeData() { - console.log("Getting Chrome data for context:", outputTarget); + console.log("[DEBUG] getChromeData called for outputTarget:", outputTarget); chrome.storage.local.get( [ + 'platform', + 'platformUsername', 'githubUsername', 'githubToken', 'projectName', @@ -86,20 +90,19 @@ function allIncluded(outputTarget = 'email') { 'orgName' ], (items) => { - console.log("Storage items received:", items); - - + console.log("[DEBUG] Storage items received:", items); + platform = items.platform || 'github'; + platformUsername = items.platformUsername || ''; + console.log(`[DEBUG] platform: ${platform}, platformUsername: ${platformUsername}`); if (outputTarget === 'popup') { const usernameFromDOM = document.getElementById('githubUsername')?.value; const projectFromDOM = document.getElementById('projectName')?.value; const reasonFromDOM = document.getElementById('userReason')?.value; const tokenFromDOM = document.getElementById('githubToken')?.value; - items.githubUsername = usernameFromDOM || items.githubUsername; items.projectName = projectFromDOM || items.projectName; items.userReason = reasonFromDOM || items.userReason; items.githubToken = tokenFromDOM || items.githubToken; - chrome.storage.local.set({ githubUsername: items.githubUsername, projectName: items.projectName, @@ -107,18 +110,15 @@ function allIncluded(outputTarget = 'email') { githubToken: items.githubToken }); } - githubUsername = items.githubUsername; projectName = items.projectName; userReason = items.userReason || 'No Blocker at the moment'; githubToken = items.githubToken; lastWeekContribution = items.lastWeekContribution; yesterdayContribution = items.yesterdayContribution; - if (!items.enableToggle) { enableToggle = items.enableToggle; } - if (items.lastWeekContribution) { handleLastWeekContributionChange(); } else if (items.yesterdayContribution) { @@ -127,33 +127,66 @@ function allIncluded(outputTarget = 'email') { startingDate = items.startingDate; endingDate = items.endingDate; } else { - handleLastWeekContributionChange(); //on fresh unpack - default to last week. + handleLastWeekContributionChange(); if (outputTarget === 'popup') { chrome.storage.local.set({ lastWeekContribution: true, yesterdayContribution: false }); } } - if (githubUsername) { - console.log("About to fetch GitHub data for:", githubUsername); - fetchGithubData(); - } else { - if (outputTarget === 'popup') { - console.log("No username found - popup context"); - // Show error in popup - const scrumReport = document.getElementById('scrumReport'); - const generateBtn = document.getElementById('generateReport'); - if (scrumReport) { - scrumReport.innerHTML = '
    Please enter your GitHub username to generate a report.
    '; - } - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; + console.log(`[DEBUG] githubUsername: ${githubUsername}, platformUsername: ${platformUsername}`); + if (platform === 'github') { + if (githubUsername) { + console.log("[DEBUG] About to fetch GitHub data for:", githubUsername); + fetchGithubData(); + } else { + if (outputTarget === 'popup') { + console.log("[DEBUG] No username found - popup context"); + const scrumReport = document.getElementById('scrumReport'); + const generateBtn = document.getElementById('generateReport'); + if (scrumReport) { + scrumReport.innerHTML = '
    Please enter your GitHub username to generate a report.
    '; + } + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + scrumGenerationInProgress = false; + } else { + console.warn('[DEBUG] No GitHub username found in storage'); + scrumGenerationInProgress = false; } - scrumGenerationInProgress = false; + return; + } + } else if (platform === 'gitlab') { + if (!gitlabHelper) gitlabHelper = new window.GitLabHelper(); + if (platformUsername) { + const generateBtn = document.getElementById('generateReport'); + gitlabHelper.fetchGitLabData(platformUsername, startingDate, endingDate) + .then(data => { + const processed = gitlabHelper.processGitLabData(data); + githubIssuesData = { items: processed.issues }; + githubPrsReviewData = { items: processed.mergeRequests }; + githubUserData = processed.user; + processGithubData({ + githubIssuesData, + githubPrsReviewData, + githubUserData + }); + }) + .catch(err => { + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; + } + }); } else { - console.warn('No GitHub username found in storage'); - scrumGenerationInProgress = false; + // Show error for missing GitLab username } - return; + } else { + console.log(`[DEBUG] Unknown platform: ${platform}`); } if (items.cacheInput) { cacheInput = items.cacheInput; @@ -363,7 +396,7 @@ function allIncluded(outputTarget = 'email') { const isCacheKeyMatch = githubCache.cacheKey === cacheKey; const needsToken = !!githubToken; const cacheUsedToken = !!githubCache.usedToken; - if (githubCache.data && isCacheFresh && isCacheKeyMatch) { + if (githubCache.data && isCacheFresh && isCacheKeyMatch) { if (needsToken && !cacheUsedToken) { log('Cache was fetched without token, but user now has a token. Invalidating cache.'); githubCache.data = null; @@ -610,26 +643,21 @@ function allIncluded(outputTarget = 'email') { } async function processGithubData(data) { - log('Processing Github data'); + log('[DEBUG] processGithubData called', data); githubIssuesData = data.githubIssuesData; githubPrsReviewData = data.githubPrsReviewData; githubUserData = data.githubUserData; - - log('GitHub data set:', { + log('[DEBUG] GitHub data set:', { issues: githubIssuesData?.items?.length || 0, prs: githubPrsReviewData?.items?.length || 0, user: githubUserData?.login }); - - lastWeekArray = []; nextWeekArray = []; reviewedPrsArray = []; githubPrsReviewDataProcessed = {}; - issuesDataProcessed = false; prsReviewDataProcessed = false; - // Update subject if (!githubCache.subject && scrumSubject) { scrumSubjectLoaded(); @@ -638,7 +666,7 @@ function allIncluded(outputTarget = 'email') { writeGithubIssuesPrs(), writeGithubPrsReviews(), ]) - log('Both data processing functions completed, generating scrum body'); + log('[DEBUG] Both data processing functions completed, generating scrum body'); writeScrumBody(); } @@ -1174,7 +1202,7 @@ if (window.location.protocol.startsWith('http')) { window.generateScrumReport = function () { allIncluded('popup'); -} +}; chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'forceRefresh') { @@ -1221,3 +1249,4 @@ ${prs.map((pr, i) => ` repo${i}: repository(owner: \"${pr.owner}\", name: \"${pr } + From 671607126e1c0fec692a660f6c751fc000f1615d Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 9 Jul 2025 23:57:00 +0530 Subject: [PATCH 12/40] fetching gitlab activities --- src/scripts/gitlabHelper.js | 43 +++++++------ src/scripts/main.js | 10 +-- src/scripts/scrumHelper.js | 125 +++++++++++++++++++++++++++--------- 3 files changed, 125 insertions(+), 53 deletions(-) diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index 80e73bf..f4a4df5 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -102,53 +102,53 @@ class GitLabHelper { // Get user info first const userUrl = `${this.baseUrl}/users?username=${username}`; + console.log('[GITLAB-DEBUG] Fetching user:', userUrl); const userRes = await fetch(userUrl); - + console.log('[GITLAB-DEBUG] User fetch response:', userRes.status, userRes.statusText); if (!userRes.ok) { throw new Error(`Error fetching GitLab user: ${userRes.status} ${userRes.statusText}`); } - const users = await userRes.json(); + console.log('[GITLAB-DEBUG] Users fetched:', users); if (users.length === 0) { throw new Error(`GitLab user '${username}' not found`); } - const userId = users[0].id; - // Fetch user's projects const projectsUrl = `${this.baseUrl}/users/${userId}/projects?per_page=100&order_by=updated_at&sort=desc`; + console.log('[GITLAB-DEBUG] Fetching projects:', projectsUrl); const projectsRes = await fetch(projectsUrl); - + console.log('[GITLAB-DEBUG] Projects fetch response:', projectsRes.status, projectsRes.statusText); if (!projectsRes.ok) { throw new Error(`Error fetching GitLab projects: ${projectsRes.status} ${projectsRes.statusText}`); } - const projects = await projectsRes.json(); - + console.log('[GITLAB-DEBUG] Projects fetched:', projects); // Fetch merge requests created by user const mergeRequestsUrl = `${this.baseUrl}/merge_requests?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; + console.log('[GITLAB-DEBUG] Fetching merge requests:', mergeRequestsUrl); const mergeRequestsRes = await fetch(mergeRequestsUrl); - + console.log('[GITLAB-DEBUG] Merge requests fetch response:', mergeRequestsRes.status, mergeRequestsRes.statusText); if (!mergeRequestsRes.ok) { throw new Error(`Error fetching GitLab merge requests: ${mergeRequestsRes.status} ${mergeRequestsRes.statusText}`); } - const mergeRequests = await mergeRequestsRes.json(); - + console.log('[GITLAB-DEBUG] Merge requests fetched:', mergeRequests); // Fetch issues created by user const issuesUrl = `${this.baseUrl}/issues?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; + console.log('[GITLAB-DEBUG] Fetching issues:', issuesUrl); const issuesRes = await fetch(issuesUrl); - + console.log('[GITLAB-DEBUG] Issues fetch response:', issuesRes.status, issuesRes.statusText); if (!issuesRes.ok) { throw new Error(`Error fetching GitLab issues: ${issuesRes.status} ${issuesRes.statusText}`); } - const issues = await issuesRes.json(); - + console.log('[GITLAB-DEBUG] Issues fetched:', issues); // Get detailed info for merge requests and issues const detailedMergeRequests = await this.getDetailedMergeRequests(mergeRequests); + console.log('[GITLAB-DEBUG] Detailed merge requests:', detailedMergeRequests); const detailedIssues = await this.getDetailedIssues(issues); - + console.log('[GITLAB-DEBUG] Detailed issues:', detailedIssues); const gitlabData = { user: users[0], projects: projects, @@ -156,7 +156,7 @@ class GitLabHelper { issues: detailedIssues, comments: [] // Empty array since we're not fetching comments }; - + console.log('[GITLAB-DEBUG] Final gitlabData object:', gitlabData); // Cache the data this.cache.data = gitlabData; this.cache.timestamp = Date.now(); @@ -185,15 +185,18 @@ class GitLabHelper { for (const mr of mergeRequests) { try { const url = `${this.baseUrl}/projects/${mr.project_id}/merge_requests/${mr.iid}`; + console.log('[GITLAB-DEBUG] Fetching detailed MR:', url); const res = await fetch(url); + console.log('[GITLAB-DEBUG] Detailed MR fetch response:', res.status, res.statusText); if (res.ok) { const detailedMr = await res.json(); + console.log('[GITLAB-DEBUG] Detailed MR data:', detailedMr); detailed.push(detailedMr); } // Add small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { - console.error(`Error fetching detailed MR ${mr.iid}:`, error); + console.error(`[GITLAB-DEBUG] Error fetching detailed MR ${mr.iid}:`, error); detailed.push(mr); // Use basic data if detailed fetch fails } } @@ -205,15 +208,18 @@ class GitLabHelper { for (const issue of issues) { try { const url = `${this.baseUrl}/projects/${issue.project_id}/issues/${issue.iid}`; + console.log('[GITLAB-DEBUG] Fetching detailed issue:', url); const res = await fetch(url); + console.log('[GITLAB-DEBUG] Detailed issue fetch response:', res.status, res.statusText); if (res.ok) { const detailedIssue = await res.json(); + console.log('[GITLAB-DEBUG] Detailed issue data:', detailedIssue); detailed.push(detailedIssue); } // Add small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { - console.error(`Error fetching detailed issue ${issue.iid}:`, error); + console.error(`[GITLAB-DEBUG] Error fetching detailed issue ${issue.iid}:`, error); detailed.push(issue); // Use basic data if detailed fetch fails } } @@ -233,7 +239,8 @@ class GitLabHelper { comments: data.comments || [], user: data.user }; - + console.log('[GITLAB-DEBUG] processGitLabData input:', data); + console.log('[GITLAB-DEBUG] processGitLabData output:', processed); console.log('GitLab data processed:', { mergeRequests: processed.mergeRequests.length, issues: processed.issues.length, diff --git a/src/scripts/main.js b/src/scripts/main.js index 383e6e2..1046a11 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -79,7 +79,7 @@ function handleBodyOnLoad() { yesterdayContributionElement.checked = true; handleYesterdayContributionChange(); } - if (items.showCommits){ + if (items.showCommits) { showCommitsElement.checked = items.showCommits; } else { showCommitsElement.checked = false; @@ -251,13 +251,15 @@ function handleUserReasonChange() { } function handleShowCommitsChange() { - let value = showCommitsElement.checked; - chrome.storage.local.set({ showCommits: value }); + let value = showCommitsElement.checked; + chrome.storage.local.set({ showCommits: value }); } enableToggleElement.addEventListener('change', handleEnableChange); githubUsernameElement.addEventListener('keyup', handleGithubUsernameChange); -githubTokenElement.addEventListener('keyup', handleGithubTokenChange); +if (githubTokenElement) { + githubTokenElement.addEventListener('keyup', handleGithubTokenChange); +} cacheInputElement.addEventListener('keyup', handleCacheInputChange); projectNameElement.addEventListener('keyup', handleProjectNameChange); startingDateElement.addEventListener('change', handleStartingDateChange); diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index c3ab8fe..e2456d8 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -162,10 +162,43 @@ function allIncluded(outputTarget = 'email') { const generateBtn = document.getElementById('generateReport'); gitlabHelper.fetchGitLabData(platformUsername, startingDate, endingDate) .then(data => { + console.log('[SCRUM-DEBUG] Raw GitLab data returned from fetchGitLabData:', data); const processed = gitlabHelper.processGitLabData(data); - githubIssuesData = { items: processed.issues }; - githubPrsReviewData = { items: processed.mergeRequests }; + console.log('[SCRUM-DEBUG] Processed GitLab data:', processed); + // Map GitLab issues and MRs to GitHub-like fields for compatibility + function mapGitLabIssue(issue) { + return { + ...issue, + repository_url: issue.web_url ? issue.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, + html_url: issue.web_url, + number: issue.iid, + state: issue.state, + title: issue.title, + body: issue.description, + pull_request: undefined // not a PR + }; + } + function mapGitLabMR(mr) { + return { + ...mr, + repository_url: mr.web_url ? mr.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, + html_url: mr.web_url, + number: mr.iid, + state: mr.state, + title: mr.title, + body: mr.description, + pull_request: {}, // mark as PR + author: mr.author // ensure author is present for authored-by check + }; + } + githubIssuesData = { items: processed.issues.map(mapGitLabIssue) }; + githubPrsReviewData = { items: processed.mergeRequests.map(mapGitLabMR) }; githubUserData = processed.user; + console.log('[SCRUM-DEBUG] Passing to processGithubData:', { + githubIssuesData, + githubPrsReviewData, + githubUserData + }); processGithubData({ githubIssuesData, githubPrsReviewData, @@ -662,10 +695,11 @@ function allIncluded(outputTarget = 'email') { if (!githubCache.subject && scrumSubject) { scrumSubjectLoaded(); } - await Promise.all([ - writeGithubIssuesPrs(), - writeGithubPrsReviews(), - ]) + log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); + await writeGithubIssuesPrs(githubIssuesData?.items || []); + log('[SCRUM-DEBUG] Processing merge requests for main activity:', githubPrsReviewData?.items); + await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); writeScrumBody(); } @@ -807,8 +841,19 @@ ${userReason}`; let i; for (i = 0; i < items.length; i++) { let item = items[i]; - if (item.user.login == githubUsername || !item.pull_request) continue; + // For GitHub: item.user.login, for GitLab: item.author?.username + let isAuthoredByUser = false; + if (platform === 'github') { + isAuthoredByUser = item.user && item.user.login === githubUsername; + } else if (platform === 'gitlab') { + isAuthoredByUser = item.author && (item.author.username === platformUsername); + } + if (isAuthoredByUser || !item.pull_request) continue; let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); let title = item.title; let number = item.number; @@ -920,13 +965,14 @@ ${userReason}`; } } - async function writeGithubIssuesPrs() { + async function writeGithubIssuesPrs(items) { log('writeGithubIssuesPrs called'); - let items = githubIssuesData.items; - lastWeekArray = []; - nextWeekArray = []; if (!items) { - logError('No Github issues data available'); + logError('No items to process for writeGithubIssuesPrs'); + return; + } + if (!items.length) { + logError('No items to process for writeGithubIssuesPrs'); return; } @@ -947,6 +993,10 @@ ${userReason}`; let item = items[i]; if (item.pull_request && item.state === 'closed' && useMergedStatus && !fallbackToSimple) { let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } let repoParts = repository_url.split('/'); let owner = repoParts[repoParts.length - 2]; let repo = repoParts[repoParts.length - 1]; @@ -978,24 +1028,33 @@ ${userReason}`; } for (let i = 0; i < items.length; i++) { let item = items[i]; + log('[SCRUM-DEBUG] Processing item:', item); + // For GitLab, treat all items in the MRs array as MRs + let isMR = !!item.pull_request; // works for both GitHub and mapped GitLab data + log('[SCRUM-DEBUG] isMR:', isMR, 'platform:', platform, 'item:', item); let html_url = item.html_url; let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); let title = item.title; let number = item.number; let li = ''; let isDraft = false; - if (item.pull_request && typeof item.draft !== 'undefined') { + if (isMR && typeof item.draft !== 'undefined') { isDraft = item.draft; } - if (item.pull_request) { - + if (isMR) { + // Platform-specific label + let prAction = ''; const prCreatedDate = new Date(item.created_at); const startDate = new Date(startingDate); const endDate = new Date(endingDate + 'T23:59:59'); const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; - - if (!isNewPR) { + if (platform === 'github') { + if (!isNewPR) { const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; if (!hasCommitsInRange) { @@ -1007,7 +1066,11 @@ ${userReason}`; } else { } - const prAction = isNewPR ? 'Made PR' : 'Existing PR'; + prAction = isNewPR ? 'Made PR' : 'Existing PR'; + + } else if (platform === 'gitlab') { + prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; + } if (isDraft) { li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_draft_button}
  • `; } else if (item.state === 'open') { @@ -1028,12 +1091,16 @@ ${userReason}`; merged = mergedStatusResults[`${owner}/${repo}#${number}`]; } if (merged === true) { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_merged_button}
  • `; + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else { // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_closed_button}
  • `; + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; } + } else { + // Fallback for unexpected state + li = `
  • (${project}) - ${prAction} (#${number}) - ${title}
  • `; } + log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); lastWeekArray.push(li); continue; } else { @@ -1058,20 +1125,14 @@ ${userReason}`; } else if (item.state === 'closed') { li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; } else { - li = - '
  • (' + - project + - ') - Opened Issue(#' + - number + - ") - " + - title + - '
  • '; + // Fallback for unexpected state + li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; } + log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item); + lastWeekArray.push(li); } - lastWeekArray.push(li); } + log('[SCRUM-DEBUG] Final lastWeekArray:', lastWeekArray); issuesDataProcessed = true; if (outputTarget === 'email') { triggerScrumGeneration(); @@ -1250,3 +1311,5 @@ ${prs.map((pr, i) => ` repo${i}: repository(owner: \"${pr.owner}\", name: \"${pr + + From 78b1a306a3914abae3bd4e398817d3d3f0a59c08 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 10 Jul 2025 00:32:07 +0530 Subject: [PATCH 13/40] ui changes --- src/popup.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/popup.html b/src/popup.html index 67ae18f..8691693 100644 --- a/src/popup.html +++ b/src/popup.html @@ -90,11 +90,15 @@

    Scrum

    From 5ef6368130080fa8de75c38f9e70ccd9a2872c3f Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 10 Jul 2025 01:25:17 +0530 Subject: [PATCH 14/40] Added correct labels --- src/scripts/gitlabHelper.js | 33 ++++++--------------------------- src/scripts/scrumHelper.js | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index f4a4df5..f18b6cb 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -52,7 +52,7 @@ class GitLabHelper { if (this.cache.fetching || (this.cache.cacheKey === cacheKey && this.cache.data)) { console.log('GitLab fetch already in progress or data already fetched. Skipping fetch.'); - return; + return this.cache.data; } console.log('Fetching GitLab data:', { @@ -102,61 +102,46 @@ class GitLabHelper { // Get user info first const userUrl = `${this.baseUrl}/users?username=${username}`; - console.log('[GITLAB-DEBUG] Fetching user:', userUrl); const userRes = await fetch(userUrl); - console.log('[GITLAB-DEBUG] User fetch response:', userRes.status, userRes.statusText); if (!userRes.ok) { throw new Error(`Error fetching GitLab user: ${userRes.status} ${userRes.statusText}`); } const users = await userRes.json(); - console.log('[GITLAB-DEBUG] Users fetched:', users); if (users.length === 0) { throw new Error(`GitLab user '${username}' not found`); } const userId = users[0].id; // Fetch user's projects const projectsUrl = `${this.baseUrl}/users/${userId}/projects?per_page=100&order_by=updated_at&sort=desc`; - console.log('[GITLAB-DEBUG] Fetching projects:', projectsUrl); const projectsRes = await fetch(projectsUrl); - console.log('[GITLAB-DEBUG] Projects fetch response:', projectsRes.status, projectsRes.statusText); if (!projectsRes.ok) { throw new Error(`Error fetching GitLab projects: ${projectsRes.status} ${projectsRes.statusText}`); } const projects = await projectsRes.json(); - console.log('[GITLAB-DEBUG] Projects fetched:', projects); // Fetch merge requests created by user const mergeRequestsUrl = `${this.baseUrl}/merge_requests?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; - console.log('[GITLAB-DEBUG] Fetching merge requests:', mergeRequestsUrl); const mergeRequestsRes = await fetch(mergeRequestsUrl); - console.log('[GITLAB-DEBUG] Merge requests fetch response:', mergeRequestsRes.status, mergeRequestsRes.statusText); if (!mergeRequestsRes.ok) { throw new Error(`Error fetching GitLab merge requests: ${mergeRequestsRes.status} ${mergeRequestsRes.statusText}`); } const mergeRequests = await mergeRequestsRes.json(); - console.log('[GITLAB-DEBUG] Merge requests fetched:', mergeRequests); // Fetch issues created by user const issuesUrl = `${this.baseUrl}/issues?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; - console.log('[GITLAB-DEBUG] Fetching issues:', issuesUrl); const issuesRes = await fetch(issuesUrl); - console.log('[GITLAB-DEBUG] Issues fetch response:', issuesRes.status, issuesRes.statusText); if (!issuesRes.ok) { throw new Error(`Error fetching GitLab issues: ${issuesRes.status} ${issuesRes.statusText}`); } const issues = await issuesRes.json(); - console.log('[GITLAB-DEBUG] Issues fetched:', issues); - // Get detailed info for merge requests and issues - const detailedMergeRequests = await this.getDetailedMergeRequests(mergeRequests); - console.log('[GITLAB-DEBUG] Detailed merge requests:', detailedMergeRequests); - const detailedIssues = await this.getDetailedIssues(issues); - console.log('[GITLAB-DEBUG] Detailed issues:', detailedIssues); + // Remove detailed fetches for MRs and issues + // const detailedMergeRequests = await this.getDetailedMergeRequests(mergeRequests); + // const detailedIssues = await this.getDetailedIssues(issues); const gitlabData = { user: users[0], projects: projects, - mergeRequests: detailedMergeRequests, - issues: detailedIssues, + mergeRequests: mergeRequests, // use list response directly + issues: issues, // use list response directly comments: [] // Empty array since we're not fetching comments }; - console.log('[GITLAB-DEBUG] Final gitlabData object:', gitlabData); // Cache the data this.cache.data = gitlabData; this.cache.timestamp = Date.now(); @@ -185,12 +170,9 @@ class GitLabHelper { for (const mr of mergeRequests) { try { const url = `${this.baseUrl}/projects/${mr.project_id}/merge_requests/${mr.iid}`; - console.log('[GITLAB-DEBUG] Fetching detailed MR:', url); const res = await fetch(url); - console.log('[GITLAB-DEBUG] Detailed MR fetch response:', res.status, res.statusText); if (res.ok) { const detailedMr = await res.json(); - console.log('[GITLAB-DEBUG] Detailed MR data:', detailedMr); detailed.push(detailedMr); } // Add small delay to avoid rate limiting @@ -208,12 +190,9 @@ class GitLabHelper { for (const issue of issues) { try { const url = `${this.baseUrl}/projects/${issue.project_id}/issues/${issue.iid}`; - console.log('[GITLAB-DEBUG] Fetching detailed issue:', url); const res = await fetch(url); - console.log('[GITLAB-DEBUG] Detailed issue fetch response:', res.status, res.statusText); if (res.ok) { const detailedIssue = await res.json(); - console.log('[GITLAB-DEBUG] Detailed issue data:', detailedIssue); detailed.push(detailedIssue); } // Add small delay to avoid rate limiting diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index e2456d8..906436f 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -132,7 +132,7 @@ function allIncluded(outputTarget = 'email') { chrome.storage.local.set({ lastWeekContribution: true, yesterdayContribution: false }); } } - console.log(`[DEBUG] githubUsername: ${githubUsername}, platformUsername: ${platformUsername}`); + if (platform === 'github') { if (githubUsername) { console.log("[DEBUG] About to fetch GitHub data for:", githubUsername); @@ -172,7 +172,7 @@ function allIncluded(outputTarget = 'email') { repository_url: issue.web_url ? issue.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, html_url: issue.web_url, number: issue.iid, - state: issue.state, + state: issue.state === "opened" ? "open" : issue.state, // normalize title: issue.title, body: issue.description, pull_request: undefined // not a PR @@ -184,7 +184,7 @@ function allIncluded(outputTarget = 'email') { repository_url: mr.web_url ? mr.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, html_url: mr.web_url, number: mr.iid, - state: mr.state, + state: mr.state === "opened" ? "open" : mr.state, // normalize title: mr.title, body: mr.description, pull_request: {}, // mark as PR @@ -219,7 +219,7 @@ function allIncluded(outputTarget = 'email') { // Show error for missing GitLab username } } else { - console.log(`[DEBUG] Unknown platform: ${platform}`); + } if (items.cacheInput) { cacheInput = items.cacheInput; @@ -1055,21 +1055,21 @@ ${userReason}`; const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; if (platform === 'github') { if (!isNewPR) { - const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; + const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; - if (!hasCommitsInRange) { + if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits + continue; //skip these prs - created outside daterange with no commits + } else { + + } } else { } - } else { - - } prAction = isNewPR ? 'Made PR' : 'Existing PR'; } else if (platform === 'gitlab') { - prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; + prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; } if (isDraft) { li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_draft_button}
  • `; @@ -1082,6 +1082,8 @@ ${userReason}`; }); } li += ``; + } else if (item.state === 'merged') { // Added for GitLab MRs + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else if (item.state === 'closed') { let merged = null; if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { From ffae45fd7807c12be871c3f33e3efafe9a50a68e Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 10 Jul 2025 15:58:13 +0530 Subject: [PATCH 15/40] enhanced ui --- src/popup.html | 17 +++++++++-------- src/scripts/popup.js | 8 +------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/popup.html b/src/popup.html index 8691693..b877bf1 100644 --- a/src/popup.html +++ b/src/popup.html @@ -78,8 +78,9 @@

    Scrum
    - -
    +

    Platform +

    +
    -

    Your GitHub Username

    +

    Your Username

    + class="w-full border-2 border-gray-200 bg-gray-200 rounded-xl text-gray-800 p-2 my-2" + placeholder="Required for fetching your contributions" style="margin-top: 0.3rem; margin-bottom: 0.8rem;">
    -

    Fetch your contributions between:

    +

    Fetch your contributions between:

    diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 9b6d7e1..5eaf275 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -630,13 +630,7 @@ document.addEventListener('DOMContentLoaded', function () { // Update UI for platform function updatePlatformUI(platform) { - if (platform === 'gitlab') { - usernameLabel.textContent = 'Your GitLab Username'; - platformUsername.placeholder = 'Required for fetching data from GitLab'; - } else { - usernameLabel.textContent = 'Your GitHub Username'; - platformUsername.placeholder = 'Required for fetching data from GitHub'; - } + // Remove the updatePlatformUI function and all calls to it, as well as any code that changes the username label or placeholder based on platform. } // On platform change From 8b224e40299312740b6770eb6e9551fde3c65ed2 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Fri, 11 Jul 2025 13:39:00 +0530 Subject: [PATCH 16/40] added --- .github/labeler.yml | 61 +++++++++++++++++ .github/workflows/labeler.yml | 13 ++++ .github/workflows/sync-upstream.yml | 96 ++++++++++++++++++++++++++ README.md | 101 ++++++++++++++++------------ src/manifest.json | 1 + 5 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..3e001e1 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,61 @@ +# Frontend changes +frontend: + - "src/**/*.html" + - "src/**/*.css" + +# JavaScript changes +javascript: + - "src/scripts/**/*.js" + - "src/**/*.js" + +# Core functionality +core: + - "src/scripts/scrumHelper.js" + - "src/scripts/emailClientAdapter.js" + +# Documentation +documentation: + - "**/*.md" + - "docs/**" + - "LICENSE" + - "README.md" + +# Configuration files +config: + - ".github/**/*" + - "*.json" + - "*.yml" + - "*.yaml" + - "package.json" + - "package-lock.json" + +# Browser extension specific +extension: + - "manifest.json" + - "src/popup.html" + - "src/scripts/main.js" + +# Testing +testing: + - "tests/**" + - "**/*.test.js" + - "**/*.spec.js" + +# Dependencies +dependencies: + - "package.json" + - "package-lock.json" + - "yarn.lock" + +# Additional labels (manually applied, no patterns) +bug: [] +codeheat: [] +duplicate: [] +enhancement: [] +good first issue: [] +hacktoberfest: [] +help wanted: [] +invalid: [] +question: [] +wontfix: [] +"Pull requests that update a dependency file": [] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..47ff27a --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,13 @@ +name: Auto Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..084972e --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,96 @@ +name: Sync Forks with Upstream + +on: + schedule: + # Run daily at 2:00 AM UTC + - cron: "0 2 * * *" + workflow_dispatch: + # Allow manual triggering + +jobs: + sync-forks: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Sync forks with upstream + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { Octokit } = require('@octokit/rest'); + + const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN + }); + + const UPSTREAM_OWNER = 'fossasia'; + const UPSTREAM_REPO = 'scrum_helper'; + + let syncedCount = 0; + let skippedCount = 0; + let errorCount = 0; + + try { + // Get all forks + console.log(`Getting forks of ${UPSTREAM_OWNER}/${UPSTREAM_REPO}...`); + const { data: forks } = await octokit.rest.repos.listForks({ + owner: UPSTREAM_OWNER, + repo: UPSTREAM_REPO, + per_page: 100 + }); + + console.log(`Found ${forks.length} forks`); + + for (const fork of forks) { + try { + console.log(`\Processing: ${fork.full_name}`); + + // Check if fork is behind upstream + const { data: comparison } = await octokit.rest.repos.compareCommits({ + owner: fork.owner.login, + repo: fork.name, + base: fork.default_branch, + head: `${UPSTREAM_OWNER}:master` + }); + + if (comparison.behind_by === 0) { + console.log(`${fork.full_name} is up to date`); + continue; + } + + if (comparison.ahead_by > 0) { + console.log(`${fork.full_name} is ahead by ${comparison.ahead_by} commits - skipping`); + skippedCount++; + continue; + } + + // Fork is only behind - safe to sync + await octokit.rest.repos.mergeUpstream({ + owner: fork.owner.login, + repo: fork.name, + branch: fork.default_branch + }); + + console.log(`Successfully synced ${fork.full_name}`); + syncedCount++; + + // Small delay to avoid rate limits + await new Promise(resolve => setTimeout(resolve, 1000)); + + } catch (error) { + errorCount++; + } + } + + // Summary + console.log(`\n Summary:`); + console.log(`Synced: ${syncedCount}`); + console.log(`Skipped: ${skippedCount}`); + console.log(`Errors: ${errorCount}`); + + } catch (error) { + console.error('Workflow failed:', error.message); + throw error; + } diff --git a/README.md b/README.md index 3d7fd60..295dc39 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ -# SCRUM Helper +# Scrum Helper -**SCRUM Helper** is a Chrome extension designed to simplify writing scrums in Google Groups for FOSSASIA projects. By adding your GitHub username, date range, and other options, it automatically fetches your PRs, Issues, and reviewed PRs via the GitHub API and pre-fills the scrum. You can then edit the scrum to fit your needs. +**Scrum Helper** is a Chrome extension that simplifies writing development reports by auto-filling content based on your Git activity. Just enter your GitHub username, select a date range, and choose your preferences, the extension automatically fetches your commits, pull requests, issues, and code reviews via the GitHub API and generates a pre-filled report that you can edit as needed. While currently focused on Git-based workflows, Scrum Helper is designed to expand to other platforms in the future. ![SCRUMLOGO](docs/images/scrumhelper-png.png) ## Features -- Automatically fetches your Git activity, including: - - **GitHub**: issues, pull requests, and code reviews - - **GitLab**: issues, merge requests, and reviewed merge requests -- Supports both **GitHub** and **GitLab** platforms -- Simple platform selection UI (checkboxes for GitHub and GitLab) +- Automatically fetches your Git activity, including commits, pull requests, issues, and code reviews. +- Currently supports GitHub, with plans to expand to other platforms - Generates editable scrum updates based on your selected date range - Integrates directly with compose windows in Google Groups, Gmail, Yahoo Mail, and Outlook -- Standalone popup interface for previewing and copying scrum reports ## How to install @@ -24,7 +20,7 @@ 3. Enable Developer Mode (toggle in the top-right) if not already. 4. Click Load unpacked and select the `src` folder inside the cloned repo 5. Click the Scrum Helper icon on your browser toolbar -6. Fill in your settings in the popup (GitHub or GitLab username, date range, etc.) +6. Fill in your settings in the popup (GitHub username, date range, etc.) + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/old_icon.png b/docs/images/old_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8925aee276a6524770811b39755c65a2de635842 GIT binary patch literal 3921 zcmeAS@N?(olHy`uVBq!ia0vp^#z1V!!2~3eXSpN*srIUnh?1bha)pAT{ItxRRE3ht zf>ednip(?yhKgIUeHVFK95`J5uYBZ{&?q~tsORDlKAryxt3RImZs?JEGZrKzDtga4D4d#iXl>f7C$nx{Iw?H)lu5_Kob!uffBJ^=yvcj_B4VT0w$sZN zO^R~8dg`m&pP0+P7l+^bfBGf2gN=@4;@q5G1_mysOrVn@e0{8v^K+E%t*n4M}uoLxJU{Ck`#}ouF>EkDFjGTJesyI<5apb&e^G)H5{r~vchqVpyMvMw4 zgB(&s`(}1BIBXI*u-Uh_y~b{L)@HTIL0lZVGk0A2emliLW2U2ml7OdDz`CAJkp;_k z?OMF{?7CYVJRK%R(`0(H_s6`Q$$9?bHz{M`fY`4o?!P}Tbv-Ju%vpm~!h22JzH8R9 zeEIfb3<9jqn`8HeKdvgWb<&*wm5=9-t*t9VpN->8K$5;LG%Py%71aAulq}k z7G0bwSpVi+%f3xat{dMLiiU?zH@mWO_nevL>&ohl%T*Y>*>rDB-PpOjt^0O=cs{fEq{oq@w;Qb&df(4Cy?^O!bg_MT z>cRBxzs>b?|F3!Z=XhV+ud`>`7!^N>o&CUXzw5rm+)h`QfGIUv3X+{7)px7|XZJ1V z)A`09WM{P0-DUHuE7^y`I)(oqnexB-F z+9+-Pv24;VKG!!cn_Az^Gl~zsw?)%5SJfcw(cR@1GKcci6>k`PIeK*Fx%|h$!J_JM zr{p7*EdQpa+eoymath>7NZYw9Dk}c{?=Ql$PftB8{$la{*=O38dFq}!sbcIb*mZc} zlxewN*G>Nzx-xUs>N~qk#U^{k-hF$9EmE;q2OM3jo0uZ!-DjL6d4?tD$A9bgG!M?r z98)*W3SGzOcqZ+l(sDL6gNaOO-CrDh&udjYdbc<6e}`kvwB{mTH3l}>ESrewABDV? znFFGOKPwpB|64tOng@q)Z)dU7373@ZH~Y5Bul`tdd4IF+W`*X;qO4cTPseWlT)fgR zQawr2AyiaUzg*_~E?bt4tv{A@`RW*-jPPP}ak{?#Yy5lpeS%jX-Mm|%pjT|sXSRLI zRyn;*H+>pPS(jwZ@=3@v$PHPv^I*cYDTgO|{J-$v!i$Wc>#7?Y64C@b)x55z9Xv8K ziBV&#O5<6EjUftMIos0azxVJyExh}*V}gg**_K|EM|oM;H#S`ENMUV6|IzL&SOo#z{++SUSE literal 0 HcmV?d00001 diff --git a/src/icons/icon.png b/src/icons/icon.png index 8925aee276a6524770811b39755c65a2de635842..5246e63dcc4e69efaebf991c842279ebebfe2906 100644 GIT binary patch literal 24952 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 literal 3921 zcmeAS@N?(olHy`uVBq!ia0vp^#z1V!!2~3eXSpN*srIUnh?1bha)pAT{ItxRRE3ht zf>ednip(?yhKgIUeHVFK95`J5uYBZ{&?q~tsORDlKAryxt3RImZs?JEGZrKzDtga4D4d#iXl>f7C$nx{Iw?H)lu5_Kob!uffBJ^=yvcj_B4VT0w$sZN zO^R~8dg`m&pP0+P7l+^bfBGf2gN=@4;@q5G1_mysOrVn@e0{8v^K+E%t*n4M}uoLxJU{Ck`#}ouF>EkDFjGTJesyI<5apb&e^G)H5{r~vchqVpyMvMw4 zgB(&s`(}1BIBXI*u-Uh_y~b{L)@HTIL0lZVGk0A2emliLW2U2ml7OdDz`CAJkp;_k z?OMF{?7CYVJRK%R(`0(H_s6`Q$$9?bHz{M`fY`4o?!P}Tbv-Ju%vpm~!h22JzH8R9 zeEIfb3<9jqn`8HeKdvgWb<&*wm5=9-t*t9VpN->8K$5;LG%Py%71aAulq}k z7G0bwSpVi+%f3xat{dMLiiU?zH@mWO_nevL>&ohl%T*Y>*>rDB-PpOjt^0O=cs{fEq{oq@w;Qb&df(4Cy?^O!bg_MT z>cRBxzs>b?|F3!Z=XhV+ud`>`7!^N>o&CUXzw5rm+)h`QfGIUv3X+{7)px7|XZJ1V z)A`09WM{P0-DUHuE7^y`I)(oqnexB-F z+9+-Pv24;VKG!!cn_Az^Gl~zsw?)%5SJfcw(cR@1GKcci6>k`PIeK*Fx%|h$!J_JM zr{p7*EdQpa+eoymath>7NZYw9Dk}c{?=Ql$PftB8{$la{*=O38dFq}!sbcIb*mZc} zlxewN*G>Nzx-xUs>N~qk#U^{k-hF$9EmE;q2OM3jo0uZ!-DjL6d4?tD$A9bgG!M?r z98)*W3SGzOcqZ+l(sDL6gNaOO-CrDh&udjYdbc<6e}`kvwB{mTH3l}>ESrewABDV? znFFGOKPwpB|64tOng@q)Z)dU7373@ZH~Y5Bul`tdd4IF+W`*X;qO4cTPseWlT)fgR zQawr2AyiaUzg*_~E?bt4tv{A@`RW*-jPPP}ak{?#Yy5lpeS%jX-Mm|%pjT|sXSRLI zRyn;*H+>pPS(jwZ@=3@v$PHPv^I*cYDTgO|{J-$v!i$Wc>#7?Y64C@b)x55z9Xv8K ziBV&#O5<6EjUftMIos0azxVJyExh}*V}gg**_K|EM|oM;H#S`ENMUV6|IzL&SOo#z{++SUSE diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 13a08e2..47ba266 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -716,9 +716,12 @@ function allIncluded(outputTarget = 'email') { scrumSubjectLoaded(); } log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); - await writeGithubIssuesPrs(githubIssuesData?.items || []); - log('[SCRUM-DEBUG] Processing merge requests for main activity:', githubPrsReviewData?.items); - await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + if (platform === 'github') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + } else if (platform === 'gitlab') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + } await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); writeScrumBody(); @@ -1087,18 +1090,11 @@ ${userReason}`; if (platform === 'github') { if (!isNewPR) { const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; - if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits - } else { - } - } else { - } prAction = isNewPR ? 'Made PR' : 'Existing PR'; - } else if (platform === 'gitlab') { prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; } @@ -1113,28 +1109,24 @@ ${userReason}`; }); } li += ``; - } else if (item.state === 'closed') { - // For GitLab, treat all closed as closed (no merged distinction) - if (platform === 'gitlab') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + } else if (platform === 'gitlab' && item.state === 'closed') { + // For GitLab, always show closed label for closed MRs + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + } else { + // GitHub: check merged status if possible + let merged = null; + if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + merged = mergedStatusResults[`${owner}/${repo}#${number}`]; + } + if (merged === true) { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else { - let merged = null; - if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - merged = mergedStatusResults[`${owner}/${repo}#${number}`]; - } - if (merged === true) { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; - } else { - // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } + // Always show closed label for merged === false or merged === null/undefined + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; } - } else { - // Fallback for unexpected state - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}
  • `; } log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); lastWeekArray.push(li); From bde12979d273a7eab638babd7ed43553867014ca Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Sun, 13 Jul 2025 16:50:17 +0530 Subject: [PATCH 21/40] fixed disable bug --- docs/images/icon128x128.png | Bin 0 -> 24952 bytes docs/images/icon128x128.svg | 65 ++++++++++++++++++++++++++++++++++++ docs/images/old_icon.png | Bin 0 -> 3921 bytes src/icons/icon.png | Bin 3921 -> 24952 bytes src/scripts/popup.js | 52 ++++++++++++++++++++++++----- src/scripts/scrumHelper.js | 58 +++++++++++++++----------------- 6 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 docs/images/icon128x128.png create mode 100644 docs/images/icon128x128.svg create mode 100644 docs/images/old_icon.png diff --git a/docs/images/icon128x128.png b/docs/images/icon128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..5246e63dcc4e69efaebf991c842279ebebfe2906 GIT binary patch literal 24952 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 literal 0 HcmV?d00001 diff --git a/docs/images/icon128x128.svg b/docs/images/icon128x128.svg new file mode 100644 index 0000000..b4406c5 --- /dev/null +++ b/docs/images/icon128x128.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/old_icon.png b/docs/images/old_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8925aee276a6524770811b39755c65a2de635842 GIT binary patch literal 3921 zcmeAS@N?(olHy`uVBq!ia0vp^#z1V!!2~3eXSpN*srIUnh?1bha)pAT{ItxRRE3ht zf>ednip(?yhKgIUeHVFK95`J5uYBZ{&?q~tsORDlKAryxt3RImZs?JEGZrKzDtga4D4d#iXl>f7C$nx{Iw?H)lu5_Kob!uffBJ^=yvcj_B4VT0w$sZN zO^R~8dg`m&pP0+P7l+^bfBGf2gN=@4;@q5G1_mysOrVn@e0{8v^K+E%t*n4M}uoLxJU{Ck`#}ouF>EkDFjGTJesyI<5apb&e^G)H5{r~vchqVpyMvMw4 zgB(&s`(}1BIBXI*u-Uh_y~b{L)@HTIL0lZVGk0A2emliLW2U2ml7OdDz`CAJkp;_k z?OMF{?7CYVJRK%R(`0(H_s6`Q$$9?bHz{M`fY`4o?!P}Tbv-Ju%vpm~!h22JzH8R9 zeEIfb3<9jqn`8HeKdvgWb<&*wm5=9-t*t9VpN->8K$5;LG%Py%71aAulq}k z7G0bwSpVi+%f3xat{dMLiiU?zH@mWO_nevL>&ohl%T*Y>*>rDB-PpOjt^0O=cs{fEq{oq@w;Qb&df(4Cy?^O!bg_MT z>cRBxzs>b?|F3!Z=XhV+ud`>`7!^N>o&CUXzw5rm+)h`QfGIUv3X+{7)px7|XZJ1V z)A`09WM{P0-DUHuE7^y`I)(oqnexB-F z+9+-Pv24;VKG!!cn_Az^Gl~zsw?)%5SJfcw(cR@1GKcci6>k`PIeK*Fx%|h$!J_JM zr{p7*EdQpa+eoymath>7NZYw9Dk}c{?=Ql$PftB8{$la{*=O38dFq}!sbcIb*mZc} zlxewN*G>Nzx-xUs>N~qk#U^{k-hF$9EmE;q2OM3jo0uZ!-DjL6d4?tD$A9bgG!M?r z98)*W3SGzOcqZ+l(sDL6gNaOO-CrDh&udjYdbc<6e}`kvwB{mTH3l}>ESrewABDV? znFFGOKPwpB|64tOng@q)Z)dU7373@ZH~Y5Bul`tdd4IF+W`*X;qO4cTPseWlT)fgR zQawr2AyiaUzg*_~E?bt4tv{A@`RW*-jPPP}ak{?#Yy5lpeS%jX-Mm|%pjT|sXSRLI zRyn;*H+>pPS(jwZ@=3@v$PHPv^I*cYDTgO|{J-$v!i$Wc>#7?Y64C@b)x55z9Xv8K ziBV&#O5<6EjUftMIos0azxVJyExh}*V}gg**_K|EM|oM;H#S`ENMUV6|IzL&SOo#z{++SUSE literal 0 HcmV?d00001 diff --git a/src/icons/icon.png b/src/icons/icon.png index 8925aee276a6524770811b39755c65a2de635842..5246e63dcc4e69efaebf991c842279ebebfe2906 100644 GIT binary patch literal 24952 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 literal 3921 zcmeAS@N?(olHy`uVBq!ia0vp^#z1V!!2~3eXSpN*srIUnh?1bha)pAT{ItxRRE3ht zf>ednip(?yhKgIUeHVFK95`J5uYBZ{&?q~tsORDlKAryxt3RImZs?JEGZrKzDtga4D4d#iXl>f7C$nx{Iw?H)lu5_Kob!uffBJ^=yvcj_B4VT0w$sZN zO^R~8dg`m&pP0+P7l+^bfBGf2gN=@4;@q5G1_mysOrVn@e0{8v^K+E%t*n4M}uoLxJU{Ck`#}ouF>EkDFjGTJesyI<5apb&e^G)H5{r~vchqVpyMvMw4 zgB(&s`(}1BIBXI*u-Uh_y~b{L)@HTIL0lZVGk0A2emliLW2U2ml7OdDz`CAJkp;_k z?OMF{?7CYVJRK%R(`0(H_s6`Q$$9?bHz{M`fY`4o?!P}Tbv-Ju%vpm~!h22JzH8R9 zeEIfb3<9jqn`8HeKdvgWb<&*wm5=9-t*t9VpN->8K$5;LG%Py%71aAulq}k z7G0bwSpVi+%f3xat{dMLiiU?zH@mWO_nevL>&ohl%T*Y>*>rDB-PpOjt^0O=cs{fEq{oq@w;Qb&df(4Cy?^O!bg_MT z>cRBxzs>b?|F3!Z=XhV+ud`>`7!^N>o&CUXzw5rm+)h`QfGIUv3X+{7)px7|XZJ1V z)A`09WM{P0-DUHuE7^y`I)(oqnexB-F z+9+-Pv24;VKG!!cn_Az^Gl~zsw?)%5SJfcw(cR@1GKcci6>k`PIeK*Fx%|h$!J_JM zr{p7*EdQpa+eoymath>7NZYw9Dk}c{?=Ql$PftB8{$la{*=O38dFq}!sbcIb*mZc} zlxewN*G>Nzx-xUs>N~qk#U^{k-hF$9EmE;q2OM3jo0uZ!-DjL6d4?tD$A9bgG!M?r z98)*W3SGzOcqZ+l(sDL6gNaOO-CrDh&udjYdbc<6e}`kvwB{mTH3l}>ESrewABDV? znFFGOKPwpB|64tOng@q)Z)dU7373@ZH~Y5Bul`tdd4IF+W`*X;qO4cTPseWlT)fgR zQawr2AyiaUzg*_~E?bt4tv{A@`RW*-jPPP}ak{?#Yy5lpeS%jX-Mm|%pjT|sXSRLI zRyn;*H+>pPS(jwZ@=3@v$PHPv^I*cYDTgO|{J-$v!i$Wc>#7?Y64C@b)x55z9Xv8K ziBV&#O5<6EjUftMIos0azxVJyExh}*V}gg**_K|EM|oM;H#S`ENMUV6|IzL&SOo#z{++SUSE diff --git a/src/scripts/popup.js b/src/scripts/popup.js index a70bec3..5f1e9cd 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -120,6 +120,7 @@ document.addEventListener('DOMContentLoaded', function () { } function updateContentState(enableToggle) { + console.log('[DEBUG] updateContentState called with:', enableToggle); const elementsToToggle = [ 'startingDate', 'endingDate', @@ -128,12 +129,15 @@ document.addEventListener('DOMContentLoaded', function () { 'copyReport', 'refreshCache', 'showOpenLabel', + 'showCommits', 'scrumReport', 'githubUsername', 'githubToken', 'projectName', + 'platformUsername', + 'orgInput', + 'cacheInput', 'settingsToggle', - ]; const radios = document.querySelectorAll('input[name="timeframe"]'); @@ -192,21 +196,36 @@ document.addEventListener('DOMContentLoaded', function () { } } - chrome.storage.local.get(['enable'], (items) => { - const enableToggle = items.enable !== false; + chrome.storage.local.get(['enableToggle'], (items) => { + console.log('[DEBUG] Storage items received:', items); + const enableToggle = items.enableToggle !== false; + console.log('[DEBUG] enableToggle calculated:', enableToggle); + + // If enableToggle is undefined (first install), set it to true by default + if (typeof items.enableToggle === 'undefined') { + console.log('[DEBUG] Setting default enableToggle to true'); + chrome.storage.local.set({ enableToggle: true }); + } + + console.log('[DEBUG] Calling updateContentState with:', enableToggle); updateContentState(enableToggle); if (!enableToggle) { + console.log('[DEBUG] Extension disabled, returning early'); return; } + console.log('[DEBUG] Extension enabled, initializing popup'); initializePopup(); }) chrome.storage.onChanged.addListener((changes, namespace) => { - if (namespace === 'local' && changes.enable) { - updateContentState(changes.enable.newValue); - if (changes.enable.newValue) { + console.log('[DEBUG] Storage changed:', changes, namespace); + if (namespace === 'local' && changes.enableToggle) { + console.log('[DEBUG] enableToggle changed to:', changes.enableToggle.newValue); + updateContentState(changes.enableToggle.newValue); + if (changes.enableToggle.newValue) { // re-initialize if enabled + console.log('[DEBUG] Re-initializing popup due to enable toggle change'); initializePopup(); } } @@ -230,7 +249,7 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.get([ 'projectName', 'orgName', 'userReason', 'showOpenLabel', 'showCommits', 'githubToken', 'cacheInput', - 'enable', 'lastWeekContribution', 'yesterdayContribution', 'startingDate', 'endingDate', 'selectedTimeframe', 'platform', 'platformUsername' + 'enableToggle', 'lastWeekContribution', 'yesterdayContribution', 'startingDate', 'endingDate', 'selectedTimeframe', 'platform', 'platformUsername' ], function (result) { if (result.projectName) projectNameInput.value = result.projectName; if (result.orgName) orgInput.value = result.orgName; @@ -239,7 +258,13 @@ document.addEventListener('DOMContentLoaded', function () { if (typeof result.showCommits !== 'undefined') showCommitsCheckbox.checked = result.showCommits; if (result.githubToken) githubTokenInput.value = result.githubToken; if (result.cacheInput) cacheInput.value = result.cacheInput; - if (typeof result.enable !== 'undefined' && enableToggleSwitch) enableToggleSwitch.checked = result.enable; + if (enableToggleSwitch) { + if (typeof result.enableToggle !== 'undefined') { + enableToggleSwitch.checked = result.enableToggle; + } else { + enableToggleSwitch.checked = true; // Default to enabled + } + } if (typeof result.lastWeekContribution !== 'undefined') lastWeekRadio.checked = result.lastWeekContribution; if (typeof result.yesterdayContribution !== 'undefined') yesterdayRadio.checked = result.yesterdayContribution; if (result.startingDate) startingDateInput.value = result.startingDate; @@ -401,8 +426,10 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.set({ cacheInput: cacheInput.value }); }); if (enableToggleSwitch) { + console.log('[DEBUG] Setting up enable toggle switch event listener'); enableToggleSwitch.addEventListener('change', function () { - chrome.storage.local.set({ enable: enableToggleSwitch.checked }); + console.log('[DEBUG] Enable toggle changed to:', enableToggleSwitch.checked); + chrome.storage.local.set({ enableToggle: enableToggleSwitch.checked }); }); } lastWeekRadio.addEventListener('change', function () { @@ -474,6 +501,13 @@ document.addEventListener('DOMContentLoaded', function () { orgInput.value = result.orgName || ''; }); + // Debug function to test storage + window.testStorage = function () { + chrome.storage.local.get(['enableToggle'], function (result) { + console.log('[TEST] Current enableToggle value:', result.enableToggle); + }); + }; + // Auto-update orgName in storage on input change orgInput.addEventListener('input', function () { let org = orgInput.value.trim().toLowerCase(); diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 13a08e2..b32ad05 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -116,7 +116,7 @@ function allIncluded(outputTarget = 'email') { githubToken = items.githubToken; lastWeekContribution = items.lastWeekContribution; yesterdayContribution = items.yesterdayContribution; - if (!items.enableToggle) { + if (typeof items.enableToggle !== 'undefined') { enableToggle = items.enableToggle; } if (items.lastWeekContribution) { @@ -716,9 +716,16 @@ function allIncluded(outputTarget = 'email') { scrumSubjectLoaded(); } log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); - await writeGithubIssuesPrs(githubIssuesData?.items || []); - log('[SCRUM-DEBUG] Processing merge requests for main activity:', githubPrsReviewData?.items); - await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + + if (platform === 'github') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + } else if (platform === 'gitlab') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + } + + + await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); writeScrumBody(); @@ -1087,18 +1094,11 @@ ${userReason}`; if (platform === 'github') { if (!isNewPR) { const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; - if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits - } else { - } - } else { - } prAction = isNewPR ? 'Made PR' : 'Existing PR'; - } else if (platform === 'gitlab') { prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; } @@ -1113,28 +1113,24 @@ ${userReason}`; }); } li += ``; - } else if (item.state === 'closed') { - // For GitLab, treat all closed as closed (no merged distinction) - if (platform === 'gitlab') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + } else if (platform === 'gitlab' && item.state === 'closed') { + // For GitLab, always show closed label for closed MRs + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + } else { + // GitHub: check merged status if possible + let merged = null; + if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + merged = mergedStatusResults[`${owner}/${repo}#${number}`]; + } + if (merged === true) { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else { - let merged = null; - if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - merged = mergedStatusResults[`${owner}/${repo}#${number}`]; - } - if (merged === true) { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; - } else { - // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } + // Always show closed label for merged === false or merged === null/undefined + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; } - } else { - // Fallback for unexpected state - li = `
  • (${project}) - ${prAction} (#${number}) - ${title}
  • `; } log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); lastWeekArray.push(li); From ff85be6fee8bf0a3bfe2ca358a9a41fc85f4575c Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Sun, 13 Jul 2025 18:28:57 +0530 Subject: [PATCH 22/40] fixed syntax bugs --- src/scripts/popup.js | 2 +- src/scripts/scrumHelper.js | 2256 ++++++++++++++++++------------------ 2 files changed, 1116 insertions(+), 1142 deletions(-) diff --git a/src/scripts/popup.js b/src/scripts/popup.js index e9d8996..fe359d0 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -1301,7 +1301,7 @@ document.addEventListener('DOMContentLoaded', function () { updatePlatformUI(platform); }); -}); + // Tooltip bubble diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index e59bce9..234a0b9 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -285,1092 +285,1063 @@ function allIncluded(outputTarget = 'email') { } getChromeData(); - function handleLastWeekContributionChange() { - endingDate = getToday(); - startingDate = getLastWeek(); - } - function handleYesterdayContributionChange() { - endingDate = getToday(); - startingDate = getYesterday(); - } - function getLastWeek() { - let today = new Date(); - let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - let lastWeekMonth = lastWeek.getMonth() + 1; - let lastWeekDay = lastWeek.getDate(); - let lastWeekYear = lastWeek.getFullYear(); - let lastWeekDisplayPadded = - ('0000' + lastWeekYear.toString()).slice(-4) + - '-' + - ('00' + lastWeekMonth.toString()).slice(-2) + - '-' + - ('00' + lastWeekDay.toString()).slice(-2); - return lastWeekDisplayPadded; - } - function getYesterday() { - let today = new Date(); - let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - let yesterdayMonth = yesterday.getMonth() + 1; - let yesterdayDay = yesterday.getDate(); - let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = - ('0000' + yesterdayYear.toString()).slice(-4) + - '-' + - ('00' + yesterdayMonth.toString()).slice(-2) + - '-' + - ('00' + yesterdayDay.toString()).slice(-2); - return yesterdayPadded; - } - function getToday() { - let today = new Date(); - let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - let WeekMonth = Week.getMonth() + 1; - let WeekDay = Week.getDate(); - let WeekYear = Week.getFullYear(); - let WeekDisplayPadded = - ('0000' + WeekYear.toString()).slice(-4) + - '-' + - ('00' + WeekMonth.toString()).slice(-2) + - '-' + - ('00' + WeekDay.toString()).slice(-2); - return WeekDisplayPadded; - } + - // Global cache object - let githubCache = { - data: null, - cacheKey: null, - timestamp: 0, - ttl: 10 * 60 * 1000, // cache valid for 10 mins - fetching: false, - queue: [], - errors: {}, - errorTTL: 60 * 1000, // 1 min error cache - subject: null, - repoData: null, - repoCacheKey: null, - repoTimeStamp: 0, - repoFetching: false, - repoQueue: [], - }; +function handleLastWeekContributionChange() { + endingDate = getToday(); + startingDate = getLastWeek(); +} +function handleYesterdayContributionChange() { + endingDate = getToday(); + startingDate = getYesterday(); +} +function getLastWeek() { + let today = new Date(); + let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + let lastWeekMonth = lastWeek.getMonth() + 1; + let lastWeekDay = lastWeek.getDate(); + let lastWeekYear = lastWeek.getFullYear(); + let lastWeekDisplayPadded = + ('0000' + lastWeekYear.toString()).slice(-4) + + '-' + + ('00' + lastWeekMonth.toString()).slice(-2) + + '-' + + ('00' + lastWeekDay.toString()).slice(-2); + return lastWeekDisplayPadded; +} +function getYesterday() { + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + let yesterdayMonth = yesterday.getMonth() + 1; + let yesterdayDay = yesterday.getDate(); + let yesterdayYear = yesterday.getFullYear(); + let yesterdayPadded = + ('0000' + yesterdayYear.toString()).slice(-4) + + '-' + + ('00' + yesterdayMonth.toString()).slice(-2) + + '-' + + ('00' + yesterdayDay.toString()).slice(-2); + return yesterdayPadded; +} +function getToday() { + let today = new Date(); + let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + let WeekMonth = Week.getMonth() + 1; + let WeekDay = Week.getDate(); + let WeekYear = Week.getFullYear(); + let WeekDisplayPadded = + ('0000' + WeekYear.toString()).slice(-4) + + '-' + + ('00' + WeekMonth.toString()).slice(-2) + + '-' + + ('00' + WeekDay.toString()).slice(-2); + return WeekDisplayPadded; +} - async function getCacheTTL() { - return new Promise((resolve) => { - chrome.storage.local.get(['cacheInput'], function (result) { - const ttlMinutes = result.cacheInput || 10; - resolve(ttlMinutes * 60 * 1000); - }); +// Global cache object +let githubCache = { + data: null, + cacheKey: null, + timestamp: 0, + ttl: 10 * 60 * 1000, // cache valid for 10 mins + fetching: false, + queue: [], + errors: {}, + errorTTL: 60 * 1000, // 1 min error cache + subject: null, + repoData: null, + repoCacheKey: null, + repoTimeStamp: 0, + repoFetching: false, + repoQueue: [], +}; + +async function getCacheTTL() { + return new Promise((resolve) => { + chrome.storage.local.get(['cacheInput'], function (result) { + const ttlMinutes = result.cacheInput || 10; + resolve(ttlMinutes * 60 * 1000); }); - } + }); +} - function saveToStorage(data, subject = null) { - const cacheData = { - data: data, - cacheKey: githubCache.cacheKey, - timestamp: githubCache.timestamp, - subject: subject, - usedToken: !!githubToken, - } - log(`Saving data to storage:`, { - cacheKey: githubCache.cacheKey, - timestamp: githubCache.timestamp, - hasSubject: !!subject, - }); +function saveToStorage(data, subject = null) { + const cacheData = { + data: data, + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + subject: subject, + usedToken: !!githubToken, + } + log(`Saving data to storage:`, { + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + hasSubject: !!subject, + }); - return new Promise((resolve) => { - chrome.storage.local.set({ githubCache: cacheData }, () => { - if (chrome.runtime.lastError) { - logError('Storage save failed: ', chrome.runtime.lastError); - resolve(false); - } else { - log('Cache saved successfuly'); - githubCache.data = data; - githubCache.subject = subject; - resolve(true); - } - }); + return new Promise((resolve) => { + chrome.storage.local.set({ githubCache: cacheData }, () => { + if (chrome.runtime.lastError) { + logError('Storage save failed: ', chrome.runtime.lastError); + resolve(false); + } else { + log('Cache saved successfuly'); + githubCache.data = data; + githubCache.subject = subject; + resolve(true); + } }); - } + }); +} - function loadFromStorage() { - log('Loading cache from storage'); - return new Promise(async (resolve) => { - const currentTTL = await getCacheTTL(); - chrome.storage.local.get('githubCache', (result) => { - const cache = result.githubCache; - if (!cache) { - log('No cache found in storage'); - resolve(false); - return; - } - const isCacheExpired = (Date.now() - cache.timestamp) > currentTTL; - if (isCacheExpired) { - log('Cached data is expired'); - resolve(false); - return; - } - log('Found valid cache:', { - cacheKey: cache.cacheKey, - age: `${((Date.now() - cache.timestamp) / 1000 / 60).toFixed(1)} minutes`, - }); +function loadFromStorage() { + log('Loading cache from storage'); + return new Promise(async (resolve) => { + const currentTTL = await getCacheTTL(); + chrome.storage.local.get('githubCache', (result) => { + const cache = result.githubCache; + if (!cache) { + log('No cache found in storage'); + resolve(false); + return; + } + const isCacheExpired = (Date.now() - cache.timestamp) > currentTTL; + if (isCacheExpired) { + log('Cached data is expired'); + resolve(false); + return; + } + log('Found valid cache:', { + cacheKey: cache.cacheKey, + age: `${((Date.now() - cache.timestamp) / 1000 / 60).toFixed(1)} minutes`, + }); - githubCache.data = cache.data; - githubCache.cacheKey = cache.cacheKey; - githubCache.timestamp = cache.timestamp; - githubCache.subject = cache.subject; - githubCache.usedToken = cache.usedToken || false; + githubCache.data = cache.data; + githubCache.cacheKey = cache.cacheKey; + githubCache.timestamp = cache.timestamp; + githubCache.subject = cache.subject; + githubCache.usedToken = cache.usedToken || false; - if (cache.subject && scrumSubject) { - scrumSubject.value = cache.subject; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - } - resolve(true); - }); + if (cache.subject && scrumSubject) { + scrumSubject.value = cache.subject; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + } + resolve(true); }); + }); +} + +async function fetchGithubData() { + const cacheKey = `${githubUsername}-${startingDate}-${endingDate}-${orgName || 'all'}`; + + if (githubCache.fetching || (githubCache.cacheKey === cacheKey && githubCache.data)) { + log('Fetch already in progress or data already fetched. Skipping fetch.'); + return; } - async function fetchGithubData() { - const cacheKey = `${githubUsername}-${startingDate}-${endingDate}-${orgName || 'all'}`; + log('Fetching Github data:', { + username: githubUsername, + startDate: startingDate, + endDate: endingDate, + }); - if (githubCache.fetching || (githubCache.cacheKey === cacheKey && githubCache.data)) { - log('Fetch already in progress or data already fetched. Skipping fetch.'); - return; - } + log('CacheKey in cache:', githubCache.cacheKey); + log('Incoming cacheKey:', cacheKey); + log('Has data:', !!githubCache.data); - log('Fetching Github data:', { - username: githubUsername, - startDate: startingDate, - endDate: endingDate, - }); + // Check if we need to load from storage + if (!githubCache.data && !githubCache.fetching) { + await loadFromStorage(); + }; - log('CacheKey in cache:', githubCache.cacheKey); - log('Incoming cacheKey:', cacheKey); - log('Has data:', !!githubCache.data); + const currentTTL = await getCacheTTL(); + githubCache.ttl = currentTTL; + log(`Caching for ${currentTTL / (60 * 1000)} minutes`); - // Check if we need to load from storage - if (!githubCache.data && !githubCache.fetching) { - await loadFromStorage(); - }; + const now = Date.now(); + const isCacheFresh = (now - githubCache.timestamp) < githubCache.ttl; + const isCacheKeyMatch = githubCache.cacheKey === cacheKey; + const needsToken = !!githubToken; + const cacheUsedToken = !!githubCache.usedToken; - const currentTTL = await getCacheTTL(); - githubCache.ttl = currentTTL; - log(`Caching for ${currentTTL / (60 * 1000)} minutes`); - - const now = Date.now(); - const isCacheFresh = (now - githubCache.timestamp) < githubCache.ttl; - const isCacheKeyMatch = githubCache.cacheKey === cacheKey; - const needsToken = !!githubToken; - const cacheUsedToken = !!githubCache.usedToken; - - if (githubCache.data && isCacheFresh && isCacheKeyMatch) { - if (needsToken && !cacheUsedToken) { - log('Cache was fetched without token, but user now has a token. Invalidating cache.'); - githubCache.data = null; - } else { - log('Using cached data - cache is fresh and key matches'); - processGithubData(githubCache.data); - return Promise.resolve(); - } - } - // if cache key does not match our cache is stale, fetch new data - if (!isCacheKeyMatch) { - log('Cache key mismatch - fetching new Data'); + if (githubCache.data && isCacheFresh && isCacheKeyMatch) { + if (needsToken && !cacheUsedToken) { + log('Cache was fetched without token, but user now has a token. Invalidating cache.'); githubCache.data = null; - } else if (!isCacheFresh) { - log('Cache is stale - fetching new data'); + } else { + log('Using cached data - cache is fresh and key matches'); + processGithubData(githubCache.data); + return Promise.resolve(); } + } + // if cache key does not match our cache is stale, fetch new data + if (!isCacheKeyMatch) { + log('Cache key mismatch - fetching new Data'); + githubCache.data = null; + } else if (!isCacheFresh) { + log('Cache is stale - fetching new data'); + } - // if fetching is in progress, queue the calls and return a promise resolved when done - if (githubCache.fetching) { - log('Fetch in progress, queuing requests'); - return new Promise((resolve, reject) => { - githubCache.queue.push({ resolve, reject }); - }); - } + // if fetching is in progress, queue the calls and return a promise resolved when done + if (githubCache.fetching) { + log('Fetch in progress, queuing requests'); + return new Promise((resolve, reject) => { + githubCache.queue.push({ resolve, reject }); + }); + } - githubCache.fetching = true; - githubCache.cacheKey = cacheKey; - githubCache.usedToken = !!githubToken; + githubCache.fetching = true; + githubCache.cacheKey = cacheKey; + githubCache.usedToken = !!githubToken; - const headers = { - 'Accept': 'application/vnd.github.v3+json', - }; + const headers = { + 'Accept': 'application/vnd.github.v3+json', + }; - if (githubToken) { - log('Making authenticated requests.'); - headers['Authorization'] = `token ${githubToken}`; + if (githubToken) { + log('Making authenticated requests.'); + headers['Authorization'] = `token ${githubToken}`; - } else { - log('Making public requests'); - } + } else { + log('Making public requests'); + } - console.log('[SCRUM-HELPER] orgName before API query:', orgName); - console.log('[SCRUM-HELPER] orgName type:', typeof orgName); - console.log('[SCRUM-HELPER] orgName length:', orgName ? orgName.length : 0); - let orgPart = orgName && orgName.trim() ? `+org%3A${orgName}` : ''; - console.log('[SCRUM-HELPER] orgPart for API:', orgPart); - console.log('[SCRUM-HELPER] orgPart length:', orgPart.length); + console.log('[SCRUM-HELPER] orgName before API query:', orgName); + console.log('[SCRUM-HELPER] orgName type:', typeof orgName); + console.log('[SCRUM-HELPER] orgName length:', orgName ? orgName.length : 0); + let orgPart = orgName && orgName.trim() ? `+org%3A${orgName}` : ''; + console.log('[SCRUM-HELPER] orgPart for API:', orgPart); + console.log('[SCRUM-HELPER] orgPart length:', orgPart.length); - let issueUrl, prUrl, userUrl; + let issueUrl, prUrl, userUrl; - if (useRepoFilter && selectedRepos && selectedRepos.length > 0) { - log('Using repo filter for api calls:', selectedRepos); + if (useRepoFilter && selectedRepos && selectedRepos.length > 0) { + log('Using repo filter for api calls:', selectedRepos); - try { - await fetchReposIfNeeded(); - } catch (err) { - logError('Failed to fetch repo data for filtering:', err); - } + try { + await fetchReposIfNeeded(); + } catch (err) { + logError('Failed to fetch repo data for filtering:', err); + } - const repoQueries = selectedRepos - .filter(repo => repo !== null) - .map(repo => { - if (typeof repo === 'object' && repo.fullName) { - // FIXED: Remove leading slash if present - const cleanName = repo.fullName.startsWith('/') ? repo.fullName.substring(1) : repo.fullName; - return `repo:${cleanName}`; - } else if (repo.includes('/')) { - // FIXED: Remove leading slash if present - const cleanName = repo.startsWith('/') ? repo.substring(1) : repo; - return `repo:${cleanName}`; - } else { - const fullRepoInfo = githubCache.repoData?.find(r => r.name === repo); - if (fullRepoInfo && fullRepoInfo.fullName) { - return `repo:${fullRepoInfo.fullName}`; - } - logError(`Missing owner for repo ${repo} - search may fail`); - return `repo:${repo}`; + const repoQueries = selectedRepos + .filter(repo => repo !== null) + .map(repo => { + if (typeof repo === 'object' && repo.fullName) { + // FIXED: Remove leading slash if present + const cleanName = repo.fullName.startsWith('/') ? repo.fullName.substring(1) : repo.fullName; + return `repo:${cleanName}`; + } else if (repo.includes('/')) { + // FIXED: Remove leading slash if present + const cleanName = repo.startsWith('/') ? repo.substring(1) : repo; + return `repo:${cleanName}`; + } else { + const fullRepoInfo = githubCache.repoData?.find(r => r.name === repo); + if (fullRepoInfo && fullRepoInfo.fullName) { + return `repo:${fullRepoInfo.fullName}`; } - }).join('+'); - - const orgQuery = orgPart ? `+${orgPart}` : ''; - issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - userUrl = `https://api.github.com/users/${githubUsername}`; - log('Repository-filtered URLs:', { issueUrl, prUrl }); - } else { - loadFromStorage('Using org wide search'); - const orgQuery = orgPart ? `+${orgPart}` : ''; - issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - userUrl = `https://api.github.com/users/${githubUsername}`; - } + logError(`Missing owner for repo ${repo} - search may fail`); + return `repo:${repo}`; + } + }).join('+'); + + const orgQuery = orgPart ? `+${orgPart}` : ''; + issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + userUrl = `https://api.github.com/users/${githubUsername}`; + log('Repository-filtered URLs:', { issueUrl, prUrl }); + } else { + loadFromStorage('Using org wide search'); + const orgQuery = orgPart ? `+${orgPart}` : ''; + issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + userUrl = `https://api.github.com/users/${githubUsername}`; + } - try { - // throttling 500ms to avoid burst - await new Promise(res => setTimeout(res, 500)); + try { + // throttling 500ms to avoid burst + await new Promise(res => setTimeout(res, 500)); - const [issuesRes, prRes, userRes] = await Promise.all([ - fetch(issueUrl, { headers }), - fetch(prUrl, { headers }), - fetch(userUrl, { headers }), - ]); + const [issuesRes, prRes, userRes] = await Promise.all([ + fetch(issueUrl, { headers }), + fetch(prUrl, { headers }), + fetch(userUrl, { headers }), + ]); - if (issuesRes.status === 401 || prRes.status === 401 || userRes.status === 401 || - issuesRes.status === 403 || prRes.status === 403 || userRes.status === 403) { - showInvalidTokenMessage(); - return; - } + if (issuesRes.status === 401 || prRes.status === 401 || userRes.status === 401 || + issuesRes.status === 403 || prRes.status === 403 || userRes.status === 403) { + showInvalidTokenMessage(); + return; + } - if (issuesRes.status === 404 || prRes.status === 404) { - if (outputTarget === 'popup') { - Materialize.toast && Materialize.toast('Organization not found on GitHub', 3000); - } - throw new Error('Organization not found'); + if (issuesRes.status === 404 || prRes.status === 404) { + if (outputTarget === 'popup') { + Materialize.toast && Materialize.toast('Organization not found on GitHub', 3000); } + throw new Error('Organization not found'); + } - if (!issuesRes.ok) throw new Error(`Error fetching Github issues: ${issuesRes.status} ${issuesRes.statusText}`); - if (!prRes.ok) throw new Error(`Error fetching Github PR review data: ${prRes.status} ${prRes.statusText}`); - if (!userRes.ok) throw new Error(`Error fetching Github userdata: ${userRes.status} ${userRes.statusText}`); - - githubIssuesData = await issuesRes.json(); - githubPrsReviewData = await prRes.json(); - githubUserData = await userRes.json(); - - if (githubIssuesData && githubIssuesData.items) { - log('Fetched githubIssuesData:', githubIssuesData.items.length, 'items'); - // Collect open PRs - const openPRs = githubIssuesData.items.filter( - item => item.pull_request && item.state === 'open' - ); - log('Open PRs for commit fetching:', openPRs.map(pr => pr.number)); - // Fetch commits for open PRs (batch) - if (openPRs.length && githubToken) { - const commitMap = await fetchCommitsForOpenPRs(openPRs, githubToken, startingDate, endingDate); - log('Commit map returned from fetchCommitsForOpenPRs:', commitMap); - // Attach commits to PR objects - openPRs.forEach(pr => { - pr._allCommits = commitMap[pr.number] || []; - log(`Attached ${pr._allCommits.length} commits to PR #${pr.number}`); - }); - } + if (!issuesRes.ok) throw new Error(`Error fetching Github issues: ${issuesRes.status} ${issuesRes.statusText}`); + if (!prRes.ok) throw new Error(`Error fetching Github PR review data: ${prRes.status} ${prRes.statusText}`); + if (!userRes.ok) throw new Error(`Error fetching Github userdata: ${userRes.status} ${userRes.statusText}`); + + githubIssuesData = await issuesRes.json(); + githubPrsReviewData = await prRes.json(); + githubUserData = await userRes.json(); + + if (githubIssuesData && githubIssuesData.items) { + log('Fetched githubIssuesData:', githubIssuesData.items.length, 'items'); + // Collect open PRs + const openPRs = githubIssuesData.items.filter( + item => item.pull_request && item.state === 'open' + ); + log('Open PRs for commit fetching:', openPRs.map(pr => pr.number)); + // Fetch commits for open PRs (batch) + if (openPRs.length && githubToken) { + const commitMap = await fetchCommitsForOpenPRs(openPRs, githubToken, startingDate, endingDate); + log('Commit map returned from fetchCommitsForOpenPRs:', commitMap); + // Attach commits to PR objects + openPRs.forEach(pr => { + pr._allCommits = commitMap[pr.number] || []; + log(`Attached ${pr._allCommits.length} commits to PR #${pr.number}`); + }); } + } - // Cache the data - githubCache.data = { githubIssuesData, githubPrsReviewData, githubUserData }; - githubCache.timestamp = Date.now(); + // Cache the data + githubCache.data = { githubIssuesData, githubPrsReviewData, githubUserData }; + githubCache.timestamp = Date.now(); - await saveToStorage(githubCache.data); - processGithubData(githubCache.data); + await saveToStorage(githubCache.data); + processGithubData(githubCache.data); - githubCache.queue.forEach(({ resolve }) => resolve()); - githubCache.queue = []; - } catch (err) { - logError('Fetch Failed:', err); - // Reject queued calls on error - githubCache.queue.forEach(({ reject }) => reject(err)); - githubCache.queue = []; - githubCache.fetching = false; + githubCache.queue.forEach(({ resolve }) => resolve()); + githubCache.queue = []; + } catch (err) { + logError('Fetch Failed:', err); + // Reject queued calls on error + githubCache.queue.forEach(({ reject }) => reject(err)); + githubCache.queue = []; + githubCache.fetching = false; - if (outputTarget === 'popup') { - const generateBtn = document.getElementById('generateReport'); - if (scrumReport) { - let errorMsg = 'An error occurred while generating the report.'; - if (err) { - if (typeof err === 'string') errorMsg = err; - else if (err.message) errorMsg = err.message; - else errorMsg = JSON.stringify(err) - } - scrumReport.innerHTML = `
    ${err.message || 'An error occurred while generating the report.'}
    `; - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; + if (outputTarget === 'popup') { + const generateBtn = document.getElementById('generateReport'); + if (scrumReport) { + let errorMsg = 'An error occurred while generating the report.'; + if (err) { + if (typeof err === 'string') errorMsg = err; + else if (err.message) errorMsg = err.message; + else errorMsg = JSON.stringify(err) } + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while generating the report.'}
    `; + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; } - scrumGenerationInProgress = false; - throw err; - } finally { - githubCache.fetching = false; } + scrumGenerationInProgress = false; + throw err; + } finally { + githubCache.fetching = false; } +} - async function fetchCommitsForOpenPRs(prs, githubToken, startDate, endDate) { - log('fetchCommitsForOpenPRs called with PRs:', prs.map(pr => pr.number), 'startDate:', startDate, 'endDate:', endDate); - if (!prs.length) return {}; - const since = new Date(startDate).toISOString(); - const until = new Date(endDate + 'T23:59:59').toISOString(); - let queries = prs.map((pr, idx) => { - const repoParts = pr.repository_url.split('/'); - const owner = repoParts[repoParts.length - 2]; - const repo = repoParts[repoParts.length - 1]; - return ` - pr${idx}: repository(owner: "${owner}", name: "${repo}") { - pullRequest(number: ${pr.number}) { - commits(first: 100) { - nodes { - commit { - messageHeadline - committedDate - url - author { - name - user { login } - } - } +async function fetchCommitsForOpenPRs(prs, githubToken, startDate, endDate) { + log('fetchCommitsForOpenPRs called with PRs:', prs.map(pr => pr.number), 'startDate:', startDate, 'endDate:', endDate); + if (!prs.length) return {}; + const since = new Date(startDate).toISOString(); + const until = new Date(endDate + 'T23:59:59').toISOString(); + let queries = prs.map((pr, idx) => { + const repoParts = pr.repository_url.split('/'); + const owner = repoParts[repoParts.length - 2]; + const repo = repoParts[repoParts.length - 1]; + return ` + pr${idx}: repository(owner: "${owner}", name: "${repo}") { + pullRequest(number: ${pr.number}) { + commits(first: 100) { + nodes { + commit { + messageHeadline + committedDate + url + author { + name + user { login } } } } + } + } + + }`; + }).join('\n'); + const query = `query { ${queries} }`; + log('GraphQL query for commits:', query); + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(githubToken ? { Authorization: `bearer ${githubToken}` } : {}) + }, + body: JSON.stringify({ query }) + }); + log('fetchCommitsForOpenPRs response status:', res.status); + const data = await res.json(); + log('fetchCommitsForOpenPRs response data:', data); + let commitMap = {}; + prs.forEach((pr, idx) => { + const prData = data.data && data.data[`pr${idx}`] && data.data[`pr${idx}`].pullRequest; + if (prData && prData.commits && prData.commits.nodes) { + const allCommits = prData.commits.nodes.map(n => n.commit); + log(`PR #${pr.number} allCommits:`, allCommits); + const filteredCommits = allCommits.filter(commit => { + const commitDate = new Date(commit.committedDate); + const sinceDate = new Date(since); + const untilDate = new Date(until); + return commitDate >= sinceDate && commitDate <= untilDate; + }); + log(`PR #${pr.number} filteredCommits:`, filteredCommits); + commitMap[pr.number] = filteredCommits; + } else { + log(`No commits found for PR #${pr.number}`); + } + }); + return commitMap; +} - }`; - }).join('\n'); - const query = `query { ${queries} }`; - log('GraphQL query for commits:', query); - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(githubToken ? { Authorization: `bearer ${githubToken}` } : {}) - }, - body: JSON.stringify({ query }) - }); - log('fetchCommitsForOpenPRs response status:', res.status); - const data = await res.json(); - log('fetchCommitsForOpenPRs response data:', data); - let commitMap = {}; - prs.forEach((pr, idx) => { - const prData = data.data && data.data[`pr${idx}`] && data.data[`pr${idx}`].pullRequest; - if (prData && prData.commits && prData.commits.nodes) { - const allCommits = prData.commits.nodes.map(n => n.commit); - log(`PR #${pr.number} allCommits:`, allCommits); - const filteredCommits = allCommits.filter(commit => { - const commitDate = new Date(commit.committedDate); - const sinceDate = new Date(since); - const untilDate = new Date(until); - return commitDate >= sinceDate && commitDate <= untilDate; - }); - log(`PR #${pr.number} filteredCommits:`, filteredCommits); - commitMap[pr.number] = filteredCommits; - } else { - log(`No commits found for PR #${pr.number}`); - } - }); - return commitMap; +async function fetchReposIfNeeded() { + if (!useRepoFilter) { + log('Repo fiter disabled, skipping fetch'); + return []; } + const repoCacheKey = `repos-${githubUsername}-${orgName}-${startingDate}-${endingDate}`; - async function fetchReposIfNeeded() { - if (!useRepoFilter) { - log('Repo fiter disabled, skipping fetch'); - return []; - } - const repoCacheKey = `repos-${githubUsername}-${orgName}-${startingDate}-${endingDate}`; - - const now = Date.now(); - const isRepoCacheFresh = (now - githubCache.repoTimeStamp) < githubCache.ttl; - const isRepoCacheKeyMatch = githubCache.repoCacheKey === repoCacheKey; + const now = Date.now(); + const isRepoCacheFresh = (now - githubCache.repoTimeStamp) < githubCache.ttl; + const isRepoCacheKeyMatch = githubCache.repoCacheKey === repoCacheKey; - if (githubCache.repoData && isRepoCacheFresh && isRepoCacheKeyMatch) { - log('Using cached repo data'); - return githubCache.repoData; - } + if (githubCache.repoData && isRepoCacheFresh && isRepoCacheKeyMatch) { + log('Using cached repo data'); + return githubCache.repoData; + } - if (githubCache.repoFetching) { - log('Repo fetch is in progress, queuing request'); - return new Promise((resolve, reject) => { - githubCache.repoQueue.push({ resolve, reject }); - }); - } + if (githubCache.repoFetching) { + log('Repo fetch is in progress, queuing request'); + return new Promise((resolve, reject) => { + githubCache.repoQueue.push({ resolve, reject }); + }); + } - githubCache.repoFetching = true; - githubCache.repoCacheKey = repoCacheKey; + githubCache.repoFetching = true; + githubCache.repoCacheKey = repoCacheKey; - try { - log('Fetching repos automatically'); - const repos = await fetchUserRepositories(githubUsername, githubToken, orgName); + try { + log('Fetching repos automatically'); + const repos = await fetchUserRepositories(githubUsername, githubToken, orgName); - githubCache.repoData = repos; - githubCache.repoTimeStamp = now; + githubCache.repoData = repos; + githubCache.repoTimeStamp = now; - chrome.storage.local.set({ - repoCache: { - data: repos, - cacheKey: repoCacheKey, - timestamp: now - } - }); + chrome.storage.local.set({ + repoCache: { + data: repos, + cacheKey: repoCacheKey, + timestamp: now + } + }); - githubCache.repoQueue.forEach(({ resolve }) => resolve(repos)); - githubCache.repoQueue = []; + githubCache.repoQueue.forEach(({ resolve }) => resolve(repos)); + githubCache.repoQueue = []; - log(`Successfuly cached ${repos.length} repositories`); - return repos; - } catch (err) { - logError('Failed to fetch reppos:', err); - githubCache.repoQueue.forEach(({ reject }) => reject(err)); - githubCache.repoQueue = []; - - throw err; - } finally { - githubCache.repoFetching = false; - } - } + log(`Successfuly cached ${repos.length} repositories`); + return repos; + } catch (err) { + logError('Failed to fetch reppos:', err); + githubCache.repoQueue.forEach(({ reject }) => reject(err)); + githubCache.repoQueue = []; - async function verifyCacheStatus() { - log('Cache Status: ', { - hasCachedData: !!githubCache.data, - cacheAge: githubCache.timestamp ? `${((Date.now() - githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : `no cache`, - cacheKey: githubCache.cacheKey, - isFetching: githubCache.fetching, - queueLength: githubCache.queue.length - }); - const storageData = await new Promise(resolve => { - chrome.storage.local.get('githubCache', resolve); - }); - log('Storage Status:', { - hasStoredData: !!storageData.githubCache, - storedCacheKey: storageData.githubCache?.cacheKey, - storageAge: storageData.githubCache?.timestamp ? - `${((Date.now() - storageData.githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : - 'no data' - }); + throw err; + } finally { + githubCache.repoFetching = false; } - verifyCacheStatus(); +} - function showInvalidTokenMessage() { - if (outputTarget === 'popup') { - const reportDiv = document.getElementById('scrumReport'); - if (reportDiv) { - reportDiv.innerHTML = '
    Invalid or expired GitHub token. Please check your token in the settings and try again.
    '; - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - } else { - alert('Invalid or expired GitHub token. Please check your token in the extension popup and try again.'); +async function verifyCacheStatus() { + log('Cache Status: ', { + hasCachedData: !!githubCache.data, + cacheAge: githubCache.timestamp ? `${((Date.now() - githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : `no cache`, + cacheKey: githubCache.cacheKey, + isFetching: githubCache.fetching, + queueLength: githubCache.queue.length + }); + const storageData = await new Promise(resolve => { + chrome.storage.local.get('githubCache', resolve); + }); + log('Storage Status:', { + hasStoredData: !!storageData.githubCache, + storedCacheKey: storageData.githubCache?.cacheKey, + storageAge: storageData.githubCache?.timestamp ? + `${((Date.now() - storageData.githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : + 'no data' + }); +} +verifyCacheStatus(); + +function showInvalidTokenMessage() { + if (outputTarget === 'popup') { + const reportDiv = document.getElementById('scrumReport'); + if (reportDiv) { + reportDiv.innerHTML = '
    Invalid or expired GitHub token. Please check your token in the settings and try again.
    '; + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; } + } else { + alert('Invalid or expired GitHub token. Please check your token in the extension popup and try again.'); } } +} - async function processGithubData(data) { +async function processGithubData(data) { - githubIssuesData = data.githubIssuesData; - githubPrsReviewData = data.githubPrsReviewData; - githubUserData = data.githubUserData; - log('[DEBUG] GitHub data set:', { - issues: githubIssuesData?.items?.length || 0, - prs: githubPrsReviewData?.items?.length || 0, - user: githubUserData?.login, - filtered: useRepoFilter - }); + githubIssuesData = data.githubIssuesData; + githubPrsReviewData = data.githubPrsReviewData; + githubUserData = data.githubUserData; + log('[DEBUG] GitHub data set:', { + issues: githubIssuesData?.items?.length || 0, + prs: githubPrsReviewData?.items?.length || 0, + user: githubUserData?.login, + filtered: useRepoFilter + }); - lastWeekArray = []; - nextWeekArray = []; - reviewedPrsArray = []; - githubPrsReviewDataProcessed = {}; - issuesDataProcessed = false; - prsReviewDataProcessed = false; + lastWeekArray = []; + nextWeekArray = []; + reviewedPrsArray = []; + githubPrsReviewDataProcessed = {}; + issuesDataProcessed = false; + prsReviewDataProcessed = false; - // Update subject + // Update subject - if (!githubCache.subject && scrumSubject) { - scrumSubjectLoaded(); - } - log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); + if (!githubCache.subject && scrumSubject) { + scrumSubjectLoaded(); + } + log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); - if (platform === 'github') { - await writeGithubIssuesPrs(githubIssuesData?.items || []); - } else if (platform === 'gitlab') { - await writeGithubIssuesPrs(githubIssuesData?.items || []); - await writeGithubIssuesPrs(githubPrsReviewData?.items || []); - } + if (platform === 'github') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + } else if (platform === 'gitlab') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + } - await writeGithubPrsReviews(); - log('[DEBUG] Both data processing functions completed, generating scrum body'); - writeScrumBody(); - } + await writeGithubPrsReviews(); + log('[DEBUG] Both data processing functions completed, generating scrum body'); + writeScrumBody(); +} - function formatDate(dateString) { - const date = new Date(dateString); - const options = { day: '2-digit', month: 'short', year: 'numeric' }; - return date.toLocaleDateString('en-US', options); - } +function formatDate(dateString) { + const date = new Date(dateString); + const options = { day: '2-digit', month: 'short', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); +} - function writeScrumBody() { - if (!enableToggle) { - scrumGenerationInProgress = false; - return; - } +function writeScrumBody() { + if (!enableToggle) { + scrumGenerationInProgress = false; + return; + } - let lastWeekUl = '
      '; - for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; - for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; - lastWeekUl += '
    '; + let lastWeekUl = '
      '; + for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; + for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; + lastWeekUl += '
    '; - let nextWeekUl = '
      '; - for (let i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; - nextWeekUl += '
    '; + let nextWeekUl = '
      '; + for (let i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; + nextWeekUl += '
    '; - let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); - let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); + let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; - let content; - if (lastWeekContribution == true || yesterdayContribution == true) { - content = `1. What did I do ${weekOrDay}?
    + let content; + if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } else { - content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    + } else { + content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } + } - if (outputTarget === 'popup') { - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - log("Found popup div, updating content"); - scrumReport.innerHTML = content; + if (outputTarget === 'popup') { + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + log("Found popup div, updating content"); + scrumReport.innerHTML = content; - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - } else { - logError('Scrum report div not found in popup'); + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; } - scrumGenerationInProgress = false; - - + } else { + logError('Scrum report div not found in popup'); } - - - const observer = new MutationObserver((mutations, obs) => { - if (!window.emailClientAdapter) { - obs.disconnect(); - return; - } - - - if (outputTarget === 'popup') { - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - log("Found popup div, updating content"); - scrumReport.innerHTML = content; - - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - } else { - logError('Scrum report div not found in popup'); - } + scrumGenerationInProgress = false; + } else if (outputTarget === 'email') { + if (hasInjectedContent) { scrumGenerationInProgress = false; + return; + } - } else if (outputTarget === 'email') { - if (hasInjectedContent) { - scrumGenerationInProgress = false; + const observer = new MutationObserver((mutations, obs) => { + if (!window.emailClientAdapter) { + obs.disconnect(); return; } - - const observer = new MutationObserver((mutations, obs) => { - if (!window.emailClientAdapter) { + if (window.emailClientAdapter.isNewConversation()) { + const elements = window.emailClientAdapter.getEditorElements(); + if (elements && elements.body) { obs.disconnect(); - return; - } - - if (window.emailClientAdapter.isNewConversation()) { - const elements = window.emailClientAdapter.getEditorElements(); - if (elements && elements.body) { - obs.disconnect(); - log('MutationObserver found the editor body. Injecting scrum content.'); - window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); - hasInjectedContent = true; - scrumGenerationInProgress = false; - } - } - - }); - - observer.observe(document.body, { - childList: true, - subtree: true - }); - - setTimeout(() => { - observer.disconnect(); - if (!hasInjectedContent && scrumGenerationInProgress) { - logError('Injection timed out after 30 seconds. The compose window might not have loaded.'); + log('MutationObserver found the editor body. Injecting scrum content.'); + window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); + hasInjectedContent = true; scrumGenerationInProgress = false; } - }, 30000); - } - } - - //load initial scrum subject - function scrumSubjectLoaded() { - try { - if (!enableToggle) return; - if (!scrumSubject) { - console.error('Subject element not found'); - return; } - setTimeout(() => { - let name = githubUserData?.name || githubUserData?.username || githubUsername || platformUsername; - let project = projectName || ''; - let curDate = new Date(); - let year = curDate.getFullYear().toString(); - let date = curDate.getDate(); - let month = curDate.getMonth(); - month++; - if (month < 10) month = '0' + month; - if (date < 10) date = '0' + date; - let dateCode = year.toString() + month.toString() + date.toString(); - - const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; - log('Generated subject:', subject); - githubCache.subject = subject; - saveToStorage(githubCache.data, subject); + }); - if (scrumSubject && scrumSubject.value !== subject) { - scrumSubject.value = subject; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - } - }); - } catch (err) { - console.error('Error while setting subject: ', err); - } + observer.observe(document.body, { + childList: true, + subtree: true + }); + + setTimeout(() => { + observer.disconnect(); + if (!hasInjectedContent && scrumGenerationInProgress) { + logError('Injection timed out after 30 seconds. The compose window might not have loaded.'); + scrumGenerationInProgress = false; + } + }, 30000); } +} - function writeGithubPrsReviews() { - let items = githubPrsReviewData.items; - log('Processing PR reviews:', { - hasItems: !!items, - itemCount: items?.length, - firstItem: items?.[0] - }); - if (!items) { - logError('No Github PR review data available'); +//load initial scrum subject +function scrumSubjectLoaded() { + try { + if (!enableToggle) return; + if (!scrumSubject) { + console.error('Subject element not found'); return; } - reviewedPrsArray = []; - githubPrsReviewDataProcessed = {}; - let i; - for (i = 0; i < items.length; i++) { - let item = items[i]; - // For GitHub: item.user.login, for GitLab: item.author?.username - let isAuthoredByUser = false; - if (platform === 'github') { - isAuthoredByUser = item.user && item.user.login === githubUsername; - } else if (platform === 'gitlab') { - isAuthoredByUser = item.author && (item.author.username === platformUsername); - } - if (isAuthoredByUser || !item.pull_request) continue; - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; + setTimeout(() => { + let name = githubUserData?.name || githubUserData?.username || githubUsername || platformUsername; + let project = projectName || ''; + let curDate = new Date(); + let year = curDate.getFullYear().toString(); + let date = curDate.getDate(); + let month = curDate.getMonth(); + month++; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + let dateCode = year.toString() + month.toString() + date.toString(); + + const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; + log('Generated subject:', subject); + githubCache.subject = subject; + saveToStorage(githubCache.data, subject); + + if (scrumSubject && scrumSubject.value !== subject) { + scrumSubject.value = subject; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); } - let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - let title = item.title; - let number = item.number; - let html_url = item.html_url; - if (!githubPrsReviewDataProcessed[project]) { - // first pr in this repo - githubPrsReviewDataProcessed[project] = []; - } - let obj = { - number: number, - html_url: html_url, - title: title, - state: item.state, - }; - githubPrsReviewDataProcessed[project].push(obj); + }); + } catch (err) { + console.error('Error while setting subject: ', err); + } +} + +function writeGithubPrsReviews() { + let items = githubPrsReviewData.items; + log('Processing PR reviews:', { + hasItems: !!items, + itemCount: items?.length, + firstItem: items?.[0] + }); + if (!items) { + logError('No Github PR review data available'); + return; + } + reviewedPrsArray = []; + githubPrsReviewDataProcessed = {}; + let i; + for (i = 0; i < items.length; i++) { + let item = items[i]; + // For GitHub: item.user.login, for GitLab: item.author?.username + let isAuthoredByUser = false; + if (platform === 'github') { + isAuthoredByUser = item.user && item.user.login === githubUsername; + } else if (platform === 'gitlab') { + isAuthoredByUser = item.author && (item.author.username === platformUsername); } - for (let repo in githubPrsReviewDataProcessed) { - let repoLi = - '
  • \ - (' + - repo + - ') - Reviewed '; - if (githubPrsReviewDataProcessed[repo].length > 1) repoLi += 'PRs - '; - else { - repoLi += 'PR - '; - } - if (githubPrsReviewDataProcessed[repo].length <= 1) { - for (let pr in githubPrsReviewDataProcessed[repo]) { - let pr_arr = githubPrsReviewDataProcessed[repo][pr]; - let prText = ''; - prText += - "#" + pr_arr.number + ' (' + pr_arr.title + ') '; - if (pr_arr.state === 'open') prText += issue_opened_button; - // Do not show closed label for reviewed PRs - prText += '  '; - repoLi += prText; - } - } else { - repoLi += '
      '; - for (let pr1 in githubPrsReviewDataProcessed[repo]) { - let pr_arr1 = githubPrsReviewDataProcessed[repo][pr1]; - let prText1 = ''; - prText1 += - "
    • #" + - pr_arr1.number + - ' (' + - pr_arr1.title + - ') '; - if (pr_arr1.state === 'open') prText1 += issue_opened_button; - // Do not show closed label for reviewed PRs - prText1 += '  
    • '; - repoLi += prText1; - } - repoLi += '
    '; - } - repoLi += '
  • '; - reviewedPrsArray.push(repoLi); + + if (isAuthoredByUser || !item.pull_request) continue; + let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; } - - prsReviewDataProcessed = true; - + let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); + let title = item.title; + let number = item.number; + let html_url = item.html_url; + if (!githubPrsReviewDataProcessed[project]) { + // first pr in this repo + githubPrsReviewDataProcessed[project] = []; + } + let obj = { + number: number, + html_url: html_url, + title: title, + state: item.state, + }; + githubPrsReviewDataProcessed[project].push(obj); } - - function triggerScrumGeneration() { - if (issuesDataProcessed && prsReviewDataProcessed) { - log('Both data sets processed, generating scrum body.'); - writeScrumBody(); + for (let repo in githubPrsReviewDataProcessed) { + let repoLi = + '
  • (' + + repo + + ') - Reviewed '; + if (githubPrsReviewDataProcessed[repo].length > 1) repoLi += 'PRs - '; + else { + repoLi += 'PR - '; + } + if (githubPrsReviewDataProcessed[repo].length <= 1) { + for (let pr in githubPrsReviewDataProcessed[repo]) { + let pr_arr = githubPrsReviewDataProcessed[repo][pr]; + let prText = ''; + prText += + "#" + pr_arr.number + ' (' + pr_arr.title + ') '; + if (pr_arr.state === 'open') prText += issue_opened_button; + // Do not show closed label for reviewed PRs + prText += '  '; + repoLi += prText; + } } else { - log('Waiting for all data to be processed before generating scrum.', { - issues: issuesDataProcessed, - reviews: prsReviewDataProcessed, - }); + repoLi += '
      '; + for (let pr1 in githubPrsReviewDataProcessed[repo]) { + let pr_arr1 = githubPrsReviewDataProcessed[repo][pr1]; + let prText1 = ''; + prText1 += + "
    • #" + + pr_arr1.number + + ' (' + + pr_arr1.title + + ') '; + if (pr_arr1.state === 'open') prText1 += issue_opened_button; + // Do not show closed label for reviewed PRs + prText1 += '  
    • '; + repoLi += prText1; + } + repoLi += '
    '; } + repoLi += '
  • '; + reviewedPrsArray.push(repoLi); } + prsReviewDataProcessed = true; +} - function getDaysBetween(start, end) { - const d1 = new Date(start); - const d2 = new Date(end); - return Math.ceil((d2 - d1) / (1000 * 60 * 60 * 24)); +function triggerScrumGeneration() { + if (issuesDataProcessed && prsReviewDataProcessed) { + log('Both data sets processed, generating scrum body.'); + writeScrumBody(); + } else { + log('Waiting for all data to be processed before generating scrum.', { + issues: issuesDataProcessed, + reviews: prsReviewDataProcessed, + }); } +} - let sessionMergedStatusCache = {}; +function getDaysBetween(start, end) { + const d1 = new Date(start); + const d2 = new Date(end); + return Math.ceil((d2 - d1) / (1000 * 60 * 60 * 24)); +} - async function fetchPrMergedStatusREST(owner, repo, number, headers) { - const cacheKey = `${owner}/${repo}#${number}`; - if (sessionMergedStatusCache[cacheKey] !== undefined) { - return sessionMergedStatusCache[cacheKey]; - } - const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`; - try { - const res = await fetch(url, { headers }); - if (!res.ok) return null; - const data = await res.json(); - const merged = !!data.merged_at; - sessionMergedStatusCache[cacheKey] = merged; - return merged; - } catch (e) { - return null; - } +let sessionMergedStatusCache = {}; + +async function fetchPrMergedStatusREST(owner, repo, number, headers) { + const cacheKey = `${owner}/${repo}#${number}`; + if (sessionMergedStatusCache[cacheKey] !== undefined) { + return sessionMergedStatusCache[cacheKey]; + } + const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`; + try { + const res = await fetch(url, { headers }); + if (!res.ok) return null; + const data = await res.json(); + const merged = !!data.merged_at; + sessionMergedStatusCache[cacheKey] = merged; + return merged; + } catch (e) { + return null; } +} - async function writeGithubIssuesPrs(items) { - log('writeGithubIssuesPrs called'); +async function writeGithubIssuesPrs(items) { + log('writeGithubIssuesPrs called'); - if (!items) { - logError('No items to process for writeGithubIssuesPrs'); - return; - } - if (!items.length) { - logError('No items to process for writeGithubIssuesPrs'); - return; - } - const headers = { 'Accept': 'application/vnd.github.v3+json' }; - if (githubToken) headers['Authorization'] = `token ${githubToken}`; - let useMergedStatus = false; - let fallbackToSimple = false; - let daysRange = getDaysBetween(startingDate, endingDate); - if (githubToken) { - useMergedStatus = true; - } else if (daysRange <= 7) { - useMergedStatus = true; - } + if (!items) { + logError('No items to process for writeGithubIssuesPrs'); + return; + } + if (!items.length) { + logError('No items to process for writeGithubIssuesPrs'); + return; + } + const headers = { 'Accept': 'application/vnd.github.v3+json' }; + if (githubToken) headers['Authorization'] = `token ${githubToken}`; + let useMergedStatus = false; + let fallbackToSimple = false; + let daysRange = getDaysBetween(startingDate, endingDate); + if (githubToken) { + useMergedStatus = true; + } else if (daysRange <= 7) { + useMergedStatus = true; + } - let prsToCheck = []; - for (let i = 0; i < items.length; i++) { - let item = items[i]; - if (item.pull_request && item.state === 'closed' && useMergedStatus && !fallbackToSimple) { - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; - } - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - prsToCheck.push({ owner, repo, number: item.number, idx: i }); + let prsToCheck = []; + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (item.pull_request && item.state === 'closed' && useMergedStatus && !fallbackToSimple) { + let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; } + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + prsToCheck.push({ owner, repo, number: item.number, idx: i }); } + } - let mergedStatusResults = {}; - if (githubToken) { - // Use GraphQL batching for all cases - if (prsToCheck.length > 0) { - mergedStatusResults = await fetchPrsMergedStatusBatch(prsToCheck, headers); + let mergedStatusResults = {}; + if (githubToken) { + // Use GraphQL batching for all cases + if (prsToCheck.length > 0) { + mergedStatusResults = await fetchPrsMergedStatusBatch(prsToCheck, headers); + } + } else if (useMergedStatus) { + if (prsToCheck.length > 30) { + fallbackToSimple = true; + if (typeof Materialize !== 'undefined' && Materialize.toast) { + Materialize.toast('API limit exceeded. Please use a GitHub token for full status. Showing only open/closed PRs.', 5000); } - } else if (useMergedStatus) { - if (prsToCheck.length > 30) { - fallbackToSimple = true; - if (typeof Materialize !== 'undefined' && Materialize.toast) { - Materialize.toast('API limit exceeded. Please use a GitHub token for full status. Showing only open/closed PRs.', 5000); - } - } else { - // Use REST API for each PR, cache results - for (let pr of prsToCheck) { - let merged = await fetchPrMergedStatusREST(pr.owner, pr.repo, pr.number, headers); - mergedStatusResults[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; - } + } else { + // Use REST API for each PR, cache results + for (let pr of prsToCheck) { + let merged = await fetchPrMergedStatusREST(pr.owner, pr.repo, pr.number, headers); + mergedStatusResults[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; } } + } - for (let i = 0; i < items.length; i++) { - let item = items[i]; - log('[SCRUM-DEBUG] Processing item:', item); - // For GitLab, treat all items in the MRs array as MRs - let isMR = !!item.pull_request; // works for both GitHub and mapped GitLab data - log('[SCRUM-DEBUG] isMR:', isMR, 'platform:', platform, 'item:', item); - let html_url = item.html_url; - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; - } - let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - let title = item.title; - let number = item.number; - let li = ''; - - let isDraft = false; - if (isMR && typeof item.draft !== 'undefined') { - isDraft = item.draft; - } + for (let i = 0; i < items.length; i++) { + let item = items[i]; + log('[SCRUM-DEBUG] Processing item:', item); + // For GitLab, treat all items in the MRs array as MRs + let isMR = !!item.pull_request; // works for both GitHub and mapped GitLab data + log('[SCRUM-DEBUG] isMR:', isMR, 'platform:', platform, 'item:', item); + let html_url = item.html_url; + let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } + let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); + let title = item.title; + let number = item.number; + let li = ''; + + let isDraft = false; + if (isMR && typeof item.draft !== 'undefined') { + isDraft = item.draft; + } - if (isMR) { - // Platform-specific label - let prAction = ''; + if (isMR) { + // Platform-specific label + let prAction = ''; - const prCreatedDate = new Date(item.created_at); - const startDate = new Date(startingDate); - const endDate = new Date(endingDate + 'T23:59:59'); - const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; + const prCreatedDate = new Date(item.created_at); + const startDate = new Date(startingDate); + const endDate = new Date(endingDate + 'T23:59:59'); + const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; - if (platform === 'github') { - if (!isNewPR) { - const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; - if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits - } + if (platform === 'github') { + if (!isNewPR) { + const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; + if (!hasCommitsInRange) { + continue; //skip these prs - created outside daterange with no commits } - prAction = isNewPR ? 'Made PR' : 'Existing PR'; - } else if (platform === 'gitlab') { - prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; } + prAction = isNewPR ? 'Made PR' : 'Existing PR'; + } else if (platform === 'gitlab') { + prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; + } - - if (isDraft) { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_draft_button}
  • `; - } else if (item.state === 'open') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_open_button}`; - if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { - log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); - item._allCommits.forEach(commit => { - li += `
  • ${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
  • `; - }); - } - li += ``; - } else if (platform === 'gitlab' && item.state === 'closed') { - // For GitLab, always show closed label for closed MRs - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } else { - // GitHub: check merged status if possible - let merged = null; - if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - merged = mergedStatusResults[`${owner}/${repo}#${number}`]; - } - if (merged === true) { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; - } else { - - // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } + if (isDraft) { + li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_draft_button}
  • `; + } else if (item.state === 'open') { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_open_button}`; + if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { + log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); + item._allCommits.forEach(commit => { + li += `
  • ${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
  • `; + }); } - log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); - lastWeekArray.push(li); - continue; // Prevent issue logic from overwriting PR li + li += ``; + } else if (platform === 'gitlab' && item.state === 'closed') { + // For GitLab, always show closed label for closed MRs + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; } else { - // Only process as issue if not a PR - if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { - let li2 = - '
  • (' + - project + - ') - Work on Issue(#' + - number + - ") - " + - title + - ' ' + - issue_opened_button + - '  
  • '; - nextWeekArray.push(li2); + // GitHub: check merged status if possible + let merged = null; + if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + merged = mergedStatusResults[`${owner}/${repo}#${number}`]; } - if (item.state === 'open') { - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; - } else if (item.state === 'closed') { - // Always show closed label for closed issues - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + if (merged === true) { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else { - // Fallback for unexpected state - li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; - } - log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item); - lastWeekArray.push(li); + // Always show closed label for merged === false or merged === null/undefined + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + + } + } + log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); + lastWeekArray.push(li); + continue; // Prevent issue logic from overwriting PR li + } else { + // Only process as issue if not a PR + if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { + let li2 = + '
  • (' + + project + + ') - Work on Issue(#' + + number + + ") - " + + title + + ' ' + + issue_opened_button + + '  
  • '; + nextWeekArray.push(li2); + } + if (item.state === 'open') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; + } else if (item.state === 'closed') { + // Always show closed label for closed issues + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + } else { + // Fallback for unexpected state + li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; } - } - log('[SCRUM-DEBUG] Final lastWeekArray:', lastWeekArray); - issuesDataProcessed = true; + log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item); + lastWeekArray.push(li); + } } + log('[SCRUM-DEBUG] Final lastWeekArray:', lastWeekArray); + issuesDataProcessed = true; +} - let intervalBody = setInterval(() => { - if (!window.emailClientAdapter) return; - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.body) return; +let intervalBody = setInterval(() => { + if (!window.emailClientAdapter) return; - clearInterval(intervalBody); - scrumBody = elements.body; - }, 500); + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.body) return; + clearInterval(intervalBody); + scrumBody = elements.body; +}, 500); - let intervalSubject = setInterval(() => { - const userData = platform === 'gitlab' ? (githubUserData || platformUsername) : githubUserData; - if (!userData || !window.emailClientAdapter) return; +let intervalSubject = setInterval(() => { + const userData = platform === 'gitlab' ? (githubUserData || platformUsername) : githubUserData; + if (!userData || !window.emailClientAdapter) return; - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.subject) return; - if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { - console.log('Not a new conversation, skipping subject interval'); - clearInterval(intervalSubject); - return; - } + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.subject) return; - clearInterval(intervalSubject); - scrumSubject = elements.subject; + if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { + console.log('Not a new conversation, skipping subject interval'); + clearInterval(intervalSubject); + return; + } - setTimeout(() => { - scrumSubjectLoaded(); - }, 500); - }, 500); + clearInterval(intervalSubject); + scrumSubject = elements.subject; + setTimeout(() => { + scrumSubjectLoaded(); + }, 500); +}, 500); - // check for github safe writing - let intervalWriteGithubIssues = setInterval(() => { - if (outputTarget === 'popup') { - return; - } else { - const username = platform === 'gitlab' ? platformUsername : githubUsername; - if (scrumBody && username && githubIssuesData && githubPrsReviewData) { - clearInterval(intervalWriteGithubIssues); - clearInterval(intervalWriteGithubPrs); - writeGithubIssuesPrs(); - } + +// check for github safe writing +let intervalWriteGithubIssues = setInterval(() => { + if (outputTarget === 'popup') { + return; + } else { + const username = platform === 'gitlab' ? platformUsername : githubUsername; + if (scrumBody && username && githubIssuesData && githubPrsReviewData) { + clearInterval(intervalWriteGithubIssues); + clearInterval(intervalWriteGithubPrs); + writeGithubIssuesPrs(); } - }, 500); - let intervalWriteGithubPrs = setInterval(() => { - if (outputTarget === 'popup') { - return; - } else { - const username = platform === 'gitlab' ? platformUsername : githubUsername; - if (scrumBody && username && githubPrsReviewData && githubIssuesData) { - clearInterval(intervalWriteGithubPrs); - clearInterval(intervalWriteGithubIssues); - writeGithubPrsReviews(); - } + } +}, 500); +let intervalWriteGithubPrs = setInterval(() => { + if (outputTarget === 'popup') { + return; + } else { + const username = platform === 'gitlab' ? platformUsername : githubUsername; + if (scrumBody && username && githubPrsReviewData && githubIssuesData) { + clearInterval(intervalWriteGithubPrs); + clearInterval(intervalWriteGithubIssues); + writeGithubPrsReviews(); + } + } + }, 500); - if (!refreshButton_Placed) { let intervalWriteButton = setInterval(() => { if (document.getElementsByClassName('F0XO1GC-x-b').length == 3 && scrumBody && enableToggle) { @@ -1390,200 +1361,203 @@ ${userReason}`; } }, 1000); } + function handleRefresh() { hasInjectedContent = false; // Reset the flag before refresh allIncluded(); - } } -} -async function forceGithubDataRefresh() { - let showCommits = false; - await new Promise(resolve => { - chrome.storage.local.get('showCommits', (result) => { - if (result.showCommits !== undefined) { - showCommits = result.showCommits; - } - resolve(); - }); - }); - if (typeof githubCache !== 'undefined') { - githubCache.data = null; - githubCache.cacheKey = null; - githubCache.timestamp = 0; - githubCache.subject = null; - githubCache.fetching = false; - githubCache.queue = []; - } - await new Promise(resolve => { - chrome.storage.local.remove('githubCache', resolve); - }); + async function forceGithubDataRefresh() { + let showCommits = false; - chrome.storage.local.set({ showCommits: showCommits }); + await new Promise(resolve => { + chrome.storage.local.get('showCommits', (result) => { + if (result.showCommits !== undefined) { + showCommits = result.showCommits; + } + resolve(); + }); + }); - hasInjectedContent = false; + if (typeof githubCache !== 'undefined') { + githubCache.data = null; + githubCache.cacheKey = null; + githubCache.timestamp = 0; + githubCache.subject = null; + githubCache.fetching = false; + githubCache.queue = []; + } - return { success: true }; -} + await new Promise(resolve => { + chrome.storage.local.remove('githubCache', resolve); + }); -if (window.location.protocol.startsWith('http')) { - allIncluded('email'); - $('button>span:contains(New conversation)').parent('button').click(() => { - allIncluded(); - }); -} + chrome.storage.local.set({ showCommits: showCommits }); -window.generateScrumReport = function () { - allIncluded('popup'); -}; + hasInjectedContent = false; -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'forceRefresh') { - forceGithubDataRefresh() - .then(result => sendResponse(result)).catch(err => { - console.error('Force refresh failed:', err); - sendResponse({ success: false, error: err.message }); - }); - return true; + return { success: true }; } -}); -async function fetchPrsMergedStatusBatch(prs, headers) { - const results = {}; - if (prs.length === 0) return results; - const query = `query { + + if (window.location.protocol.startsWith('http')) { + allIncluded('email'); + $('button>span:contains(New conversation)').parent('button').click(() => { + allIncluded(); + }); + } + + window.generateScrumReport = function () { + allIncluded('popup'); + }; + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'forceRefresh') { + forceGithubDataRefresh() + .then(result => sendResponse(result)).catch(err => { + console.error('Force refresh failed:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; + } + }); + + async function fetchPrsMergedStatusBatch(prs, headers) { + const results = {}; + if (prs.length === 0) return results; + const query = `query { ${prs.map((pr, i) => ` repo${i}: repository(owner: \"${pr.owner}\", name: \"${pr.repo}\") { pr${i}: pullRequest(number: ${pr.number}) { merged } }`).join('\n')} }`; - try { - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query }), - }); - if (!res.ok) return results; - const data = await res.json(); - prs.forEach((pr, i) => { - const merged = data.data[`repo${i}`]?.[`pr${i}`]?.merged; - results[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; - }); - return results; - } catch (e) { - return results; - } + try { + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + if (!res.ok) return results; + const data = await res.json(); + prs.forEach((pr, i) => { + const merged = data.data[`repo${i}`]?.[`pr${i}`]?.merged; + results[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; + }); + return results; + } catch (e) { + return results; + } -} + } -let selectedRepos = []; -let useRepoFilter = false; + let selectedRepos = []; + let useRepoFilter = false; -async function fetchUserRepositories(username, token, org = '') { - const headers = { - 'Accept': 'application/vnd.github.v3+json', - }; + async function fetchUserRepositories(username, token, org = '') { + const headers = { + 'Accept': 'application/vnd.github.v3+json', + }; - if (token) { - headers['Authorization'] = `token ${token}`; - } + if (token) { + headers['Authorization'] = `token ${token}`; + } - if (!username) { - throw new Error('GitHub username is required'); - } + if (!username) { + throw new Error('GitHub username is required'); + } - console.log('Fetching repos for username:', username, 'org:', org); + console.log('Fetching repos for username:', username, 'org:', org); - try { - let dateRange = ''; try { - const storageData = await new Promise(resolve => { - chrome.storage.local.get(['startingDate', 'endingDate', 'lastWeekContribution', 'yesterdayContribution'], resolve); - }); + let dateRange = ''; + try { + const storageData = await new Promise(resolve => { + chrome.storage.local.get(['startingDate', 'endingDate', 'lastWeekContribution', 'yesterdayContribution'], resolve); + }); - let startDate, endDate; - if (storageData.lastWeekContribution) { - const today = new Date(); - const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - startDate = lastWeek.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; - } else if (storageData.yesterdayContribution) { - const today = new Date(); - const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - startDate = yesterday.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; - } else if (storageData.startingDate && storageData.endingDate) { - startDate = storageData.startingDate; - endDate = storageData.endingDate; - } else { + let startDate, endDate; + if (storageData.lastWeekContribution) { + const today = new Date(); + const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + startDate = lastWeek.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; + } else if (storageData.yesterdayContribution) { + const today = new Date(); + const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + startDate = yesterday.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; + } else if (storageData.startingDate && storageData.endingDate) { + startDate = storageData.startingDate; + endDate = storageData.endingDate; + } else { + const today = new Date(); + const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + startDate = lastWeek.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; + } + + dateRange = `+created:${startDate}..${endDate}`; + console.log(`Using date range for repo search: ${startDate} to ${endDate}`); + } catch (err) { + console.warn('Could not determine date range, using last 30 days:', err); const today = new Date(); - const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - startDate = lastWeek.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; + const thirtyDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 30); + const startDate = thirtyDaysAgo.toISOString().split('T')[0]; + const endDate = today.toISOString().split('T')[0]; } + let orgPart = org && org !== 'all' ? `+org:${org}` : ''; + const issuesUrl = `https://api.github.com/search/issues?q=author:${username}${orgPart}${dateRange}&per_page=100`; + const commentsUrl = `https://api.github.com/search/issues?q=commenter:${username}${orgPart}${dateRange.replace('created:', 'updated:')}&per_page=100`; - dateRange = `+created:${startDate}..${endDate}`; - console.log(`Using date range for repo search: ${startDate} to ${endDate}`); - } catch (err) { - console.warn('Could not determine date range, using last 30 days:', err); - const today = new Date(); - const thirtyDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 30); - const startDate = thirtyDaysAgo.toISOString().split('T')[0]; - const endDate = today.toISOString().split('T')[0]; - } - let orgPart = org && org !== 'all' ? `+org:${org}` : ''; - const issuesUrl = `https://api.github.com/search/issues?q=author:${username}${orgPart}${dateRange}&per_page=100`; - const commentsUrl = `https://api.github.com/search/issues?q=commenter:${username}${orgPart}${dateRange.replace('created:', 'updated:')}&per_page=100`; - - console.log('Search URLs:', { issuesUrl, commentsUrl }); + console.log('Search URLs:', { issuesUrl, commentsUrl }); - const [issuesRes, commentsRes] = await Promise.all([ - fetch(issuesUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })), - fetch(commentsUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })) - ]); + const [issuesRes, commentsRes] = await Promise.all([ + fetch(issuesUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })), + fetch(commentsUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })) + ]); - let repoSet = new Set(); + let repoSet = new Set(); - const processRepoItems = (items) => { - items?.forEach(item => { - if (item.repository_url) { - const urlParts = item.repository_url.split('/'); - const repoFullName = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`; - const repoName = `${urlParts[urlParts.length - 1]}` - repoSet.add(repoFullName); - } - }) - } + const processRepoItems = (items) => { + items?.forEach(item => { + if (item.repository_url) { + const urlParts = item.repository_url.split('/'); + const repoFullName = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`; + const repoName = `${urlParts[urlParts.length - 1]}` + repoSet.add(repoFullName); + } + }) + } - if (issuesRes.ok) { - const issuesData = await issuesRes.json(); - processRepoItems(issuesData.items); - console.log(`Found ${issuesData.items?.length || 0} issues/PRs authored by user in date range`); - } + if (issuesRes.ok) { + const issuesData = await issuesRes.json(); + processRepoItems(issuesData.items); + console.log(`Found ${issuesData.items?.length || 0} issues/PRs authored by user in date range`); + } - if (commentsRes.ok) { - const commentsData = await commentsRes.json(); - processRepoItems(commentsData.items); - console.log(`Found ${commentsData.items?.length || 0} issues/PRs with user comments in date range`); - } + if (commentsRes.ok) { + const commentsData = await commentsRes.json(); + processRepoItems(commentsData.items); + console.log(`Found ${commentsData.items?.length || 0} issues/PRs with user comments in date range`); + } - const repoNames = Array.from(repoSet); - console.log(`Found ${repoNames.length} unique repositories with contributions in the selected date range`); + const repoNames = Array.from(repoSet); + console.log(`Found ${repoNames.length} unique repositories with contributions in the selected date range`); - if (repoNames.length === 0) { - console.log(`No repositories with contrbutions found in the selected date range`); - return []; - } + if (repoNames.length === 0) { + console.log(`No repositories with contrbutions found in the selected date range`); + return []; + } - const repoFields = ` + const repoFields = ` name nameWithOwner description @@ -1594,90 +1568,90 @@ async function fetchUserRepositories(username, token, org = '') { } `; - const repoQueries = repoNames.slice(0, 50).map((repoFullName, i) => { - const parts = repoFullName.split('/'); - if (parts.length !== 2) return ''; - const owner = parts[0]; - const repo = parts[1]; - return ` + const repoQueries = repoNames.slice(0, 50).map((repoFullName, i) => { + const parts = repoFullName.split('/'); + if (parts.length !== 2) return ''; + const owner = parts[0]; + const repo = parts[1]; + return ` repo${i}: repository(owner: "${owner}", name: "${repo}") { ... on Repository { ${repoFields} } } `; - }).join('\n'); + }).join('\n'); - const query = `query { ${repoQueries} }`; + const query = `query { ${repoQueries} }`; - try { - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query }), - }); + try { + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); - if (!res.ok) { - throw new Error(`GraphQL request for repos failed: ${res.status}`); - } + if (!res.ok) { + throw new Error(`GraphQL request for repos failed: ${res.status}`); + } - const graphQLData = await res.json(); + const graphQLData = await res.json(); - if (graphQLData.errors) { - logError("GraphQL errors fetching repos:", graphQLData.errors); - return []; - } + if (graphQLData.errors) { + logError("GraphQL errors fetching repos:", graphQLData.errors); + return []; + } - const repos = Object.values(graphQLData.data) - .filter(repo => repo !== null) - .map(repo => ({ - name: repo.name, - fullName: repo.nameWithOwner, - description: repo.description, - language: repo.primaryLanguage ? repo.primaryLanguage.name : null, - updatedAt: repo.pushedAt, - stars: repo.stargazerCount - })); + const repos = Object.values(graphQLData.data) + .filter(repo => repo !== null) + .map(repo => ({ + name: repo.name, + fullName: repo.nameWithOwner, + description: repo.description, + language: repo.primaryLanguage ? repo.primaryLanguage.name : null, + updatedAt: repo.pushedAt, + stars: repo.stargazerCount + })); - console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); - return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); + console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); + return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); + } catch (err) { + logError('Failed to fetch repository details via GraphQL:', err); + throw err; + } } catch (err) { - logError('Failed to fetch repository details via GraphQL:', err); + logError('Failed to fetch repositories:', err); throw err; } - } catch (err) { - logError('Failed to fetch repositories:', err); - throw err; - } -} - -function filterDataByRepos(data, selectedRepos) { - if (!selectedRepos || selectedRepos.length === 0) { - return data; } - const filteredData = { - ...data, - githubIssuesData: { - ...data.githubIssuesData, - items: data.githubIssuesData?.items?.filter(item => { - const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); - return selectedRepos.includes(repoName); - }) || [] - }, - githubPrsReviewData: { - ...data.githubPrsReviewData, - items: data.githubPrsReviewData?.items?.filter(item => { - const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); - return selectedRepos.includes(repoName); - }) || [] + function filterDataByRepos(data, selectedRepos) { + if (!selectedRepos || selectedRepos.length === 0) { + return data; } - }; - return filteredData; -} -window.fetchUserRepositories = fetchUserRepositories; + + const filteredData = { + ...data, + githubIssuesData: { + ...data.githubIssuesData, + items: data.githubIssuesData?.items?.filter(item => { + const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); + return selectedRepos.includes(repoName); + }) || [] + }, + githubPrsReviewData: { + ...data.githubPrsReviewData, + items: data.githubPrsReviewData?.items?.filter(item => { + const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); + return selectedRepos.includes(repoName); + }) || [] + } + }; + return filteredData; + } + window.fetchUserRepositories = fetchUserRepositories; From a645e97d1a2591b794051d82c3858618bc59d6c8 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 01:09:26 +0530 Subject: [PATCH 23/40] fixed github report genration --- src/scripts/main.js | 16 +- src/scripts/popup.js | 685 ++++++----- src/scripts/scrumHelper.js | 2370 ++++++++++++++++++------------------ 3 files changed, 1517 insertions(+), 1554 deletions(-) diff --git a/src/scripts/main.js b/src/scripts/main.js index 5f0c65b..b131bf0 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,5 +1,5 @@ let enableToggleElement = document.getElementById('enable'); -let githubUsernameElement = document.getElementById('githubUsername'); +let platformUsernameElement = document.getElementById('platformUsername'); let githubTokenElement = document.getElementById('githubToken'); let cacheInputElement = document.getElementById('cacheInput'); let projectNameElement = document.getElementById('projectName'); @@ -14,7 +14,7 @@ let showCommitsElement = document.getElementById('showCommits'); function handleBodyOnLoad() { chrome.storage.local.get( [ - 'githubUsername', + 'platformUsername', 'projectName', 'enableToggle', 'startingDate', @@ -28,8 +28,8 @@ function handleBodyOnLoad() { 'showCommits', ], (items) => { - if (items.githubUsername) { - githubUsernameElement.value = items.githubUsername; + if (items.platformUsername) { + platformUsernameElement.value = items.platformUsername; } if (items.githubToken) { githubTokenElement.value = items.githubToken; @@ -212,9 +212,9 @@ function getToday() { return WeekDisplayPadded; } -function handleGithubUsernameChange() { - let value = githubUsernameElement.value; - chrome.storage.local.set({ githubUsername: value }); +function handlePlatformUsernameChange() { + let value = platformUsernameElement.value; + chrome.storage.local.set({ platformUsername: value }); } function handleGithubTokenChange() { let value = githubTokenElement.value; @@ -251,7 +251,7 @@ function handleShowCommitsChange() { } enableToggleElement.addEventListener('change', handleEnableChange); -githubUsernameElement.addEventListener('keyup', handleGithubUsernameChange); +platformUsernameElement.addEventListener('keyup', handlePlatformUsernameChange); if (githubTokenElement) { githubTokenElement.addEventListener('keyup', handleGithubTokenChange); } diff --git a/src/scripts/popup.js b/src/scripts/popup.js index fe359d0..25b0a9f 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -1,6 +1,6 @@ function debounce(func, wait) { let timeout; - return function(...args){ + return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); } @@ -79,7 +79,7 @@ document.addEventListener('DOMContentLoaded', function () { const githubTokenInput = document.getElementById('githubToken'); const tokenWarning = document.getElementById('tokenWarningForFilter'); - if(!useRepoFilter || !githubTokenInput || !tokenWarning) { + if (!useRepoFilter || !githubTokenInput || !tokenWarning) { return; } const isFilterEnabled = useRepoFilter.checked; @@ -254,12 +254,12 @@ document.addEventListener('DOMContentLoaded', function () { initializePopup(); } } - if(changes.startingDate || changes.endingDate) { + if (changes.startingDate || changes.endingDate) { console.log('[POPUP-DEBUG] Date changed in storage, triggering repo fetch.', { startingDate: changes.startingDate?.newValue, endingDate: changes.endingDate?.newValue }); - if(window.triggerRepoFetchIfEnabled){ + if (window.triggerRepoFetchIfEnabled) { window.triggerRepoFetchIfEnabled(); } } @@ -555,17 +555,17 @@ document.addEventListener('DOMContentLoaded', function () { const useRepoFilter = document.getElementById('useRepoFilter'); const repoFilterContainer = document.getElementById('repoFilterContainer'); - if(repoSearch && useRepoFilter && repoFilterContainer) { + if (repoSearch && useRepoFilter && repoFilterContainer) { repoSearch.addEventListener('click', function () { - if(!useRepoFilter.checked){ + if (!useRepoFilter.checked) { useRepoFilter.checked = true; - repoFilterContainer.classList.remove('hidden'); + repoFilterContainer.classList.remove('hidden'); chrome.storage.local.set({ useRepoFilter: true }); } }) } - if(!repoSearch || !useRepoFilter) { + if (!repoSearch || !useRepoFilter) { console.log('Repository, filter elements not found in DOM'); } else { @@ -587,30 +587,30 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.get(['repoCache'], resolve); }); const items = await new Promise(resolve => { - chrome.storage.local.get(['githubUsername', 'githubToken', 'orgName'], resolve); + chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve); }); - if (!items.githubUsername) { + if (!items.platformUsername) { if (repoStatus) { - repoStatus.textContent = 'GitHub username required'; + repoStatus.textContent = 'Username required'; } return; } if (window.fetchUserRepositories) { const repos = await window.fetchUserRepositories( - items.githubUsername, + items.platformUsername, items.githubToken, items.orgName || '' ); - + availableRepos = repos; - + if (repoStatus) { repoStatus.textContent = `${repos.length} repositories loaded`; } - const repoCacheKey = `repos-${items.githubUsername}-${items.orgName || ''}`; + const repoCacheKey = `repos-${items.platformUsername}-${items.orgName || ''}`; chrome.storage.local.set({ repoCache: { data: repos, @@ -638,11 +638,11 @@ document.addEventListener('DOMContentLoaded', function () { window.triggerRepoFetchIfEnabled = triggerRepoFetchIfEnabled; chrome.storage.local.get(['selectedRepos', 'useRepoFilter'], (items) => { - if(items.selectedRepos) { + if (items.selectedRepos) { selectedRepos = items.selectedRepos; updateRepoDisplay(); } - if(items.useRepoFilter){ + if (items.useRepoFilter) { useRepoFilter.checked = items.useRepoFilter; repoFilterContainer.classList.toggle('hidden', !items.useRepoFilter); } @@ -653,10 +653,10 @@ document.addEventListener('DOMContentLoaded', function () { const hasToken = githubTokenInput.value.trim() !== ''; repoFilterContainer.classList.toggle('hidden', !enabled); - if(enabled && !hasToken) { + if (enabled && !hasToken) { useRepoFilter.checked = false; const tokenWarning = document.getElementById('tokenWarningForFilter'); - if(tokenWarning) { + if (tokenWarning) { tokenWarning.classList.remove('hidden'); tokenWarning.classList.add('shake-animation'); setTimeout(() => tokenWarning.classList.remove('shake-animation'), 620); @@ -673,72 +673,72 @@ document.addEventListener('DOMContentLoaded', function () { githubCache: null, //forces refresh }); checkTokenForFilter(); - if(enabled) { - repoStatus.textContent = 'Loading repos automatically..'; + if (enabled) { + repoStatus.textContent = 'Loading repos automatically..'; - try{ - const cacheData = await new Promise(resolve => { - chrome.storage.local.get(['repoCache'], resolve); - }); - const items = await new Promise(resolve => { - chrome.storage.local.get(['githubUsername', 'githubToken', 'orgName'], resolve); - }); - - if(!items.githubUsername){ - repoStatus.textContent = 'Github Username required'; - return; - } + try { + const cacheData = await new Promise(resolve => { + chrome.storage.local.get(['repoCache'], resolve); + }); + const items = await new Promise(resolve => { + chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve); + }); - const repoCacheKey = `repos-${items.githubUsername}-${items.orgName || ''}`; - const now = Date.now(); - const cacheAge = cacheData.repoCache?.timestamp ? now - cacheData.repoCache.timestamp : Infinity; - const cacheTTL = 10 * 60 * 1000; // 10 minutes - - if (cacheData.repoCache && - cacheData.repoCache.cacheKey === repoCacheKey && - cacheAge < cacheTTL) { - - console.log('Using cached repositories'); - availableRepos = cacheData.repoCache.data; - repoStatus.textContent = `${availableRepos.length} repositories loaded`; - - if(document.activeElement === repoSearch){ - filterAndDisplayRepos(repoSearch.value.toLowerCase()); + if (!items.platformUsername) { + repoStatus.textContent = 'Github Username required'; + return; } - return; - } - if(window.fetchUserRepositories){ - const repos = await window.fetchUserRepositories( - items.githubUsername, - items.githubToken, - items.orgName || '', - ); - availableRepos = repos; - repoStatus.textContent = `${repos.length} repositories loaded`; + const repoCacheKey = `repos-${items.platformUsername}-${items.orgName || ''}`; + const now = Date.now(); + const cacheAge = cacheData.repoCache?.timestamp ? now - cacheData.repoCache.timestamp : Infinity; + const cacheTTL = 10 * 60 * 1000; // 10 minutes - chrome.storage.local.set({ - repoCache: { - data: repos, - cacheKey: repoCacheKey, - timestamp: now + if (cacheData.repoCache && + cacheData.repoCache.cacheKey === repoCacheKey && + cacheAge < cacheTTL) { + + console.log('Using cached repositories'); + availableRepos = cacheData.repoCache.data; + repoStatus.textContent = `${availableRepos.length} repositories loaded`; + + if (document.activeElement === repoSearch) { + filterAndDisplayRepos(repoSearch.value.toLowerCase()); } - }); + return; + } - if(document.activeElement === repoSearch){ - filterAndDisplayRepos(repoSearch.value.toLowerCase()); + if (window.fetchUserRepositories) { + const repos = await window.fetchUserRepositories( + items.platformUsername, + items.githubToken, + items.orgName || '', + ); + availableRepos = repos; + repoStatus.textContent = `${repos.length} repositories loaded`; + + chrome.storage.local.set({ + repoCache: { + data: repos, + cacheKey: repoCacheKey, + timestamp: now + } + }); + + if (document.activeElement === repoSearch) { + filterAndDisplayRepos(repoSearch.value.toLowerCase()); + } + } + } catch (err) { + console.error('Auto load repos failed', err); + if (err.message?.includes('401')) { + repoStatus.textContent = 'Github token required for private repos'; + } else if (err.message?.includes('username')) { + repoStatus.textContent = 'Please enter your Github username first'; + } else { + repoStatus.textContent = `Error: ${err.message || 'Failed to load repos'}`; } } - } catch(err) { - console.error('Auto load repos failed', err); - if(err.message?.includes('401')) { - repoStatus.textContent = 'Github token required for private repos'; - } else if( err.message?.includes('username')){ - repoStatus.textContent = 'Please enter your Github username first'; - } else { - repoStatus.textContent = `Error: ${err.message || 'Failed to load repos'}`; - } - } } else { selectedRepos = []; updateRepoDisplay(); @@ -750,20 +750,20 @@ document.addEventListener('DOMContentLoaded', function () { repoSearch.addEventListener('keydown', (e) => { const items = repoDropdown.querySelectorAll('.repository-dropdown-item'); - switch(e.key) { + switch (e.key) { case 'ArrowDown': e.preventDefault(); - highlightedIndex = Math.min(highlightedIndex+ 1, items.length - 1); + highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1); updateHighlight(items); break; case 'ArrowUp': e.preventDefault(); - highlightedIndex = Math.max(highlightedIndex-1, 0); + highlightedIndex = Math.max(highlightedIndex - 1, 0); updateHighlight(items); break; case 'Enter': e.preventDefault(); - if(highlightedIndex >= 0 && items[highlightedIndex]) { + if (highlightedIndex >= 0 && items[highlightedIndex]) { fnSelectedRepos(items[highlightedIndex].dataset.repoName); } break; @@ -779,7 +779,7 @@ document.addEventListener('DOMContentLoaded', function () { }) let programmaticFocus = false; repoSearch.addEventListener('focus', function () { - if(programmaticFocus){ + if (programmaticFocus) { programmaticFocus = false; return; } @@ -791,15 +791,15 @@ document.addEventListener('DOMContentLoaded', function () { }); document.addEventListener('click', (e) => { - if(!e.target.closest('#repoSearch') && !e.target.closest('#repoDropdown')) { + if (!e.target.closest('#repoSearch') && !e.target.closest('#repoDropdown')) { hideDropdown(); } }); function debugRepoFetch() { - chrome.storage.local.get(['githubUsername', 'githubToken', 'orgName'], (items) => { + chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], (items) => { console.log('Current settings:', { - username: items.githubUsername, + username: items.platformUsername, hasToken: !!items.githubToken, org: items.orgName || '' }); @@ -809,21 +809,21 @@ document.addEventListener('DOMContentLoaded', function () { async function loadRepos() { console.log('window.fetchUserRepositories exists:', !!window.fetchUserRepositories); console.log('Available functions:', Object.keys(window).filter(key => key.includes('fetch'))); - - if(!window.fetchUserRepositories) { + + if (!window.fetchUserRepositories) { repoStatus.textContent = 'Repository fetching not available'; return; } - chrome.storage.local.get(['githubUsername', 'githubToken'], (items) => { + chrome.storage.local.get(['platformUsername', 'githubToken'], (items) => { console.log('Storage data for repo fetch:', { - hasUsername: !!items.githubUsername, + hasUsername: !!items.platformUsername, hasToken: !!items.githubToken, - username: items.githubUsername + username: items.platformUsername }); - - if (!items.githubUsername) { - repoStatus.textContent = 'GitHub username required'; + + if (!items.platformUsername) { + repoStatus.textContent = 'Username required'; return; } @@ -840,10 +840,10 @@ document.addEventListener('DOMContentLoaded', function () { const cacheData = await new Promise(resolve => { chrome.storage.local.get(['repoCache'], resolve); }); - const storageItems = await new Promise( resolve => { - chrome.storage.local.get(['githubUsername', 'githubToken', 'orgName'], resolve); + const storageItems = await new Promise(resolve => { + chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve) }) - const repoCacheKey = `repos-${storageItems.githubUsername}-${storageItems.orgName || ''}`; + const repoCacheKey = `repos-${storageItems.platformUsername}-${storageItems.orgName || ''}`; const now = Date.now(); const cacheAge = cacheData.repoCache?.timestamp ? now - cacheData.repoCache.timestamp : Infinity; const cacheTTL = 10 * 60 * 1000; // 10 minutes @@ -856,23 +856,23 @@ document.addEventListener('DOMContentLoaded', function () { isFresh: cacheAge < cacheTTL }); - if (cacheData.repoCache && - cacheData.repoCache.cacheKey === repoCacheKey && + if (cacheData.repoCache && + cacheData.repoCache.cacheKey === repoCacheKey && cacheAge < cacheTTL) { - + console.log('[POPUP-DEBUG] Using cached repositories in manual fetch'); availableRepos = cacheData.repoCache.data; repoStatus.textContent = `${availableRepos.length} repositories loaded`; - - if(document.activeElement === repoSearch){ + + if (document.activeElement === repoSearch) { filterAndDisplayRepos(repoSearch.value.toLowerCase()); } return; } console.log('[POPUP-DEBUG] No valid cache. Fetching from network.'); availableRepos = await window.fetchUserRepositories( - storageItems.githubUsername, - storageItems.githubToken, + storageItems.platformUsername, + storageItems.githubToken, storageItems.orgName || '' ); repoStatus.textContent = `${availableRepos.length} repositories loaded`; @@ -886,12 +886,12 @@ document.addEventListener('DOMContentLoaded', function () { } }); - if(document.activeElement === repoSearch){ + if (document.activeElement === repoSearch) { filterAndDisplayRepos(repoSearch.value.toLowerCase()); } } catch (err) { console.error(`Failed to load repos:`, err); - + if (err.message && err.message.includes('401')) { repoStatus.textContent = 'GitHub token required for private repos'; } else if (err.message && err.message.includes('username')) { @@ -899,23 +899,23 @@ document.addEventListener('DOMContentLoaded', function () { } else { repoStatus.textContent = `Error: ${err.message || 'Failed to load repositories'}`; } - } finally{ + } finally { repoSearch.classList.remove('repository-search-loading'); } } function filterAndDisplayRepos(query) { - if(availableRepos.length === 0) { + if (availableRepos.length === 0) { repoDropdown.innerHTML = '
    Loading repositories automatically...
    '; showDropdown(); return; } - const filtered = availableRepos.filter(repo => + const filtered = availableRepos.filter(repo => !selectedRepos.includes(repo.name) && (repo.name.toLowerCase().includes(query) || repo.description?.toLowerCase().includes(query)) ); - if(filtered.length === 0) { + if (filtered.length === 0) { repoDropdown.innerHTML = '
    No repositories found
    '; } else { repoDropdown.innerHTML = filtered.slice(0, 10).map(repo => ` @@ -943,7 +943,7 @@ document.addEventListener('DOMContentLoaded', function () { } function fnSelectedRepos(repoName) { - if(!selectedRepos.includes(repoName)){ + if (!selectedRepos.includes(repoName)) { selectedRepos.push(repoName); updateRepoDisplay(); saveRepoSelection(); @@ -956,17 +956,17 @@ document.addEventListener('DOMContentLoaded', function () { } function removeRepo(repoName) { - selectedRepos = selectedRepos.filter(name => name !== repoName) ; + selectedRepos = selectedRepos.filter(name => name !== repoName); updateRepoDisplay(); saveRepoSelection(); - if(repoSearch.value) { + if (repoSearch.value) { filterAndDisplayRepos(repoSearch.value.toLowerCase()); } } - function updateRepoDisplay(){ - if(selectedRepos.length === 0) { + function updateRepoDisplay() { + if (selectedRepos.length === 0) { repoTags.innerHTML = 'No repositories selected (all will be included)'; repoCount.textContent = ' 0 repositories selected'; } else { @@ -1005,127 +1005,67 @@ document.addEventListener('DOMContentLoaded', function () { repoDropdown.classList.add('hidden'); highlightedIndex = -1; } - + function updateHighlight(items) { items.forEach((item, index) => { item.classList.toggle('highlighted', index === highlightedIndex); }); - if(highlightedIndex >= 0 && items[highlightedIndex]) { + if (highlightedIndex >= 0 && items[highlightedIndex]) { items[highlightedIndex].scrollIntoView({ block: 'nearest' }); } } window.removeRepo = removeRepo; - chrome.storage.local.get(['githubUsername'], (items) => { - if(items.githubUsername && useRepoFilter.checked && availableRepos.length === 0) { + chrome.storage.local.get(['platformUsername'], (items) => { + if (items.platformUsername && useRepoFilter.checked && availableRepos.length === 0) { setTimeout(() => loadRepos(), 1000); } }) } }); - // Auto-update orgName in storage on input change - orgInput.addEventListener('input', function () { - let org = orgInput.value.trim().toLowerCase(); - // Allow empty org to fetch all GitHub activities - chrome.storage.local.set({ orgName: org }, function () { - chrome.storage.local.remove('githubCache'); // Clear cache on org change - }); +// Auto-update orgName in storage on input change +orgInput.addEventListener('input', function () { + let org = orgInput.value.trim().toLowerCase(); + // Allow empty org to fetch all GitHub activities + chrome.storage.local.set({ orgName: org }, function () { + chrome.storage.local.remove('githubCache'); // Clear cache on org change }); +}); - // Add click event for setOrgBtn to set org - setOrgBtn.addEventListener('click', function () { - let org = orgInput.value.trim().toLowerCase(); - // Do not default to any org, allow empty string - // if (!org) { - // org = 'fossasia'; - // } - console.log('[Org Check] Checking organization:', org); - if (!org) { - // If org is empty, clear orgName in storage but don't auto-generate report - chrome.storage.local.set({ orgName: '' }, function () { - console.log('[Org Check] Organization cleared from storage'); - const scrumReport = document.getElementById('scrumReport'); - if(scrumReport){ - scrumReport.innerHTML = `

    Organization cleared. Click Generate button to fetch all your GitHub activities.

    `; - } - chrome.storage.local.remove(['githubCache', 'repoCache']); - triggerRepoFetchIfEnabled(); - setOrgBtn.disabled = false; - setOrgBtn.innerHTML = originalText; - }); - return; - } - - setOrgBtn.disabled = true; - const originalText = setOrgBtn.innerHTML; - setOrgBtn.innerHTML = ''; - - fetch(`https://api.github.com/orgs/${org}`) - .then(res => { - if (res.status === 404) { - setOrgBtn.disabled = false; - setOrgBtn.innerHTML = originalText; - const oldToast = document.getElementById('invalid-org-toast'); - if (oldToast) oldToast.parentNode.removeChild(oldToast); - const toastDiv = document.createElement('div'); - toastDiv.id = 'invalid-org-toast'; - toastDiv.className = 'toast'; - toastDiv.style.background = '#dc2626'; - toastDiv.style.color = '#fff'; - toastDiv.style.fontWeight = 'bold'; - toastDiv.style.padding = '12px 24px'; - toastDiv.style.borderRadius = '8px'; - toastDiv.style.position = 'fixed'; - toastDiv.style.top = '24px'; - toastDiv.style.left = '50%'; - toastDiv.style.transform = 'translateX(-50%)'; - toastDiv.style.zIndex = '9999'; - toastDiv.innerText = 'Organization not found on GitHub.'; - document.body.appendChild(toastDiv); - setTimeout(() => { - if (toastDiv.parentNode) toastDiv.parentNode.removeChild(toastDiv); - }, 3000); - return; - } - const oldToast = document.getElementById('invalid-org-toast'); - if (oldToast) oldToast.parentNode.removeChild(oldToast); +// Add click event for setOrgBtn to set org +setOrgBtn.addEventListener('click', function () { + let org = orgInput.value.trim().toLowerCase(); + // Do not default to any org, allow empty string + // if (!org) { + // org = 'fossasia'; + // } + console.log('[Org Check] Checking organization:', org); + if (!org) { + // If org is empty, clear orgName in storage but don't auto-generate report + chrome.storage.local.set({ orgName: '' }, function () { + console.log('[Org Check] Organization cleared from storage'); + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = `

    Organization cleared. Click Generate button to fetch all your GitHub activities.

    `; + } + chrome.storage.local.remove(['githubCache', 'repoCache']); + triggerRepoFetchIfEnabled(); + setOrgBtn.disabled = false; + setOrgBtn.innerHTML = originalText; + }); + return; + } - chrome.storage.local.set({ orgName: org }, function () { - // Always clear the scrum report and show org changed message - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - scrumReport.innerHTML = '

    Organization changed. Click Generate button to fetch the GitHub activities.

    '; - } - // Clear the githubCache for previous org - chrome.storage.local.remove('githubCache'); - setOrgBtn.disabled = false; - setOrgBtn.innerHTML = originalText; - // Always show green toast: org is set - const toastDiv = document.createElement('div'); - toastDiv.id = 'invalid-org-toast'; - toastDiv.className = 'toast'; - toastDiv.style.background = '#10b981'; - toastDiv.style.color = '#fff'; - toastDiv.style.fontWeight = 'bold'; - toastDiv.style.padding = '12px 24px'; - toastDiv.style.borderRadius = '8px'; - toastDiv.style.position = 'fixed'; - toastDiv.style.top = '24px'; - toastDiv.style.left = '50%'; - toastDiv.style.transform = 'translateX(-50%)'; - toastDiv.style.zIndex = '9999'; - toastDiv.innerText = 'Organization is set.'; - document.body.appendChild(toastDiv); - setTimeout(() => { - if (toastDiv.parentNode) toastDiv.parentNode.removeChild(toastDiv); - }, 2500); + setOrgBtn.disabled = true; + const originalText = setOrgBtn.innerHTML; + setOrgBtn.innerHTML = ''; - }); - }) - .catch((err) => { + fetch(`https://api.github.com/orgs/${org}`) + .then(res => { + if (res.status === 404) { setOrgBtn.disabled = false; setOrgBtn.innerHTML = originalText; const oldToast = document.getElementById('invalid-org-toast'); @@ -1143,92 +1083,205 @@ document.addEventListener('DOMContentLoaded', function () { toastDiv.style.left = '50%'; toastDiv.style.transform = 'translateX(-50%)'; toastDiv.style.zIndex = '9999'; - toastDiv.innerText = 'Error validating organization.'; + toastDiv.innerText = 'Organization not found on GitHub.'; document.body.appendChild(toastDiv); setTimeout(() => { if (toastDiv.parentNode) toastDiv.parentNode.removeChild(toastDiv); }, 3000); - }); - }); - - let cacheInput = document.getElementById('cacheInput'); - if (cacheInput) { - chrome.storage.local.get(['cacheInput'], function (result) { - if (result.cacheInput) { - cacheInput.value = result.cacheInput; - } else { - cacheInput.value = 10; + return; } - }); + const oldToast = document.getElementById('invalid-org-toast'); + if (oldToast) oldToast.parentNode.removeChild(oldToast); - cacheInput.addEventListener('blur', function () { - let ttlValue = parseInt(this.value); - if (isNaN(ttlValue) || ttlValue <= 0 || this.value.trim() === '') { - ttlValue = 10; - this.value = ttlValue; - this.style.borderColor = '#ef4444'; - } else if (ttlValue > 1440) { - ttlValue = 1440; - this.value = ttlValue; - this.style.borderColor = '#f59e0b'; - } else { - this.style.borderColor = '#10b981'; - } + chrome.storage.local.set({ orgName: org }, function () { + // Always clear the scrum report and show org changed message + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = '

    Organization changed. Click Generate button to fetch the GitHub activities.

    '; + } + // Clear the githubCache for previous org + chrome.storage.local.remove('githubCache'); + setOrgBtn.disabled = false; + setOrgBtn.innerHTML = originalText; + // Always show green toast: org is set + const toastDiv = document.createElement('div'); + toastDiv.id = 'invalid-org-toast'; + toastDiv.className = 'toast'; + toastDiv.style.background = '#10b981'; + toastDiv.style.color = '#fff'; + toastDiv.style.fontWeight = 'bold'; + toastDiv.style.padding = '12px 24px'; + toastDiv.style.borderRadius = '8px'; + toastDiv.style.position = 'fixed'; + toastDiv.style.top = '24px'; + toastDiv.style.left = '50%'; + toastDiv.style.transform = 'translateX(-50%)'; + toastDiv.style.zIndex = '9999'; + toastDiv.innerText = 'Organization is set.'; + document.body.appendChild(toastDiv); + setTimeout(() => { + if (toastDiv.parentNode) toastDiv.parentNode.removeChild(toastDiv); + }, 2500); - chrome.storage.local.set({ cacheInput: ttlValue }, function () { - console.log('Cache TTL saved:', ttlValue, 'minutes'); }); + }) + .catch((err) => { + setOrgBtn.disabled = false; + setOrgBtn.innerHTML = originalText; + const oldToast = document.getElementById('invalid-org-toast'); + if (oldToast) oldToast.parentNode.removeChild(oldToast); + const toastDiv = document.createElement('div'); + toastDiv.id = 'invalid-org-toast'; + toastDiv.className = 'toast'; + toastDiv.style.background = '#dc2626'; + toastDiv.style.color = '#fff'; + toastDiv.style.fontWeight = 'bold'; + toastDiv.style.padding = '12px 24px'; + toastDiv.style.borderRadius = '8px'; + toastDiv.style.position = 'fixed'; + toastDiv.style.top = '24px'; + toastDiv.style.left = '50%'; + toastDiv.style.transform = 'translateX(-50%)'; + toastDiv.style.zIndex = '9999'; + toastDiv.innerText = 'Error validating organization.'; + document.body.appendChild(toastDiv); + setTimeout(() => { + if (toastDiv.parentNode) toastDiv.parentNode.removeChild(toastDiv); + }, 3000); }); - } +}); +let cacheInput = document.getElementById('cacheInput'); +if (cacheInput) { + chrome.storage.local.get(['cacheInput'], function (result) { + if (result.cacheInput) { + cacheInput.value = result.cacheInput; + } else { + cacheInput.value = 10; + } + }); + + cacheInput.addEventListener('blur', function () { + let ttlValue = parseInt(this.value); + if (isNaN(ttlValue) || ttlValue <= 0 || this.value.trim() === '') { + ttlValue = 10; + this.value = ttlValue; + this.style.borderColor = '#ef4444'; + } else if (ttlValue > 1440) { + ttlValue = 1440; + this.value = ttlValue; + this.style.borderColor = '#f59e0b'; + } else { + this.style.borderColor = '#10b981'; + } - // Restore platform from storage or default to github - chrome.storage.local.get(['platform'], function (result) { - const platform = result.platform || 'github'; - platformSelect.value = platform; - updatePlatformUI(platform); + chrome.storage.local.set({ cacheInput: ttlValue }, function () { + console.log('Cache TTL saved:', ttlValue, 'minutes'); + }); }); +} + + +// Restore platform from storage or default to github +chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + platformSelect.value = platform; + updatePlatformUI(platform); +}); + +// Update UI for platform +function updatePlatformUI(platform) { + // (Optional) You can update the label/placeholder here if you want + // Do NOT clear the username field here, only do it on actual platform change +} - // Update UI for platform - function updatePlatformUI(platform) { - // Remove the updatePlatformUI function and all calls to it, as well as any code that changes the username label or placeholder based on platform. +// On platform change +platformSelect.addEventListener('change', function () { + const platform = platformSelect.value; + chrome.storage.local.set({ platform }); + // Clear username field and storage on platform change + const platformUsername = document.getElementById('platformUsername'); + if (platformUsername) { + platformUsername.value = ''; + chrome.storage.local.set({ platformUsername: '' }); } + updatePlatformUI(platform); +}); - // On platform change - platformSelect.addEventListener('change', function () { - const platform = platformSelect.value; - chrome.storage.local.set({ platform }); - updatePlatformUI(platform); - }); +// Custom platform dropdown logic +const customDropdown = document.getElementById('customPlatformDropdown'); +const dropdownBtn = document.getElementById('platformDropdownBtn'); +const dropdownList = document.getElementById('platformDropdownList'); +const dropdownSelected = document.getElementById('platformDropdownSelected'); +const platformSelectHidden = document.getElementById('platformSelect'); + +function setPlatformDropdown(value) { + if (value === 'gitlab') { + dropdownSelected.innerHTML = ' GitLab'; + } else { + dropdownSelected.innerHTML = ' GitHub'; + } + platformSelectHidden.value = value; + chrome.storage.local.set({ platform: value }); + // Always clear username when user changes platform + const platformUsername = document.getElementById('platformUsername'); + if (platformUsername) { + platformUsername.value = ''; + chrome.storage.local.set({ platformUsername: '' }); + } + updatePlatformUI(value); +} + +dropdownBtn.addEventListener('click', function (e) { + e.stopPropagation(); + customDropdown.classList.toggle('open'); + dropdownList.classList.toggle('hidden'); +}); - // Custom platform dropdown logic - const customDropdown = document.getElementById('customPlatformDropdown'); - const dropdownBtn = document.getElementById('platformDropdownBtn'); - const dropdownList = document.getElementById('platformDropdownList'); - const dropdownSelected = document.getElementById('platformDropdownSelected'); - const platformSelectHidden = document.getElementById('platformSelect'); +dropdownList.querySelectorAll('li').forEach(item => { + item.addEventListener('click', function (e) { + const newPlatform = this.getAttribute('data-value'); + const currentPlatform = platformSelectHidden.value; - function setPlatformDropdown(value) { - if (value === 'gitlab') { - dropdownSelected.innerHTML = ' GitLab'; - } else { - dropdownSelected.innerHTML = ' GitHub'; + // Only clear username if platform is actually changing + if (newPlatform !== currentPlatform) { + platformUsername.value = ''; + chrome.storage.local.set({ platformUsername: '' }); } - platformSelectHidden.value = value; - chrome.storage.local.set({ platform: value }); - updatePlatformUI(value); - // Don't clear username when restoring platform from storage - // Only clear when user actually changes platform - } - dropdownBtn.addEventListener('click', function (e) { - e.stopPropagation(); - customDropdown.classList.toggle('open'); - dropdownList.classList.toggle('hidden'); + setPlatformDropdown(newPlatform); + customDropdown.classList.remove('open'); + dropdownList.classList.add('hidden'); }); +}); + +document.addEventListener('click', function (e) { + if (!customDropdown.contains(e.target)) { + customDropdown.classList.remove('open'); + dropdownList.classList.add('hidden'); + } +}); - dropdownList.querySelectorAll('li').forEach(item => { - item.addEventListener('click', function (e) { +// Keyboard navigation +platformDropdownBtn.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + customDropdown.classList.add('open'); + dropdownList.classList.remove('hidden'); + dropdownList.querySelector('li').focus(); + } +}); +dropdownList.querySelectorAll('li').forEach((item, idx, arr) => { + item.setAttribute('tabindex', '0'); + item.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + (arr[idx + 1] || arr[0]).focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + (arr[idx - 1] || arr[arr.length - 1]).focus(); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); const newPlatform = this.getAttribute('data-value'); const currentPlatform = platformSelectHidden.value; @@ -1241,65 +1294,23 @@ document.addEventListener('DOMContentLoaded', function () { setPlatformDropdown(newPlatform); customDropdown.classList.remove('open'); dropdownList.classList.add('hidden'); - }); - }); - - document.addEventListener('click', function (e) { - if (!customDropdown.contains(e.target)) { - customDropdown.classList.remove('open'); - dropdownList.classList.add('hidden'); - } - }); - - // Keyboard navigation - platformDropdownBtn.addEventListener('keydown', function (e) { - if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - customDropdown.classList.add('open'); - dropdownList.classList.remove('hidden'); - dropdownList.querySelector('li').focus(); + dropdownBtn.focus(); } }); - dropdownList.querySelectorAll('li').forEach((item, idx, arr) => { - item.setAttribute('tabindex', '0'); - item.addEventListener('keydown', function (e) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - (arr[idx + 1] || arr[0]).focus(); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - (arr[idx - 1] || arr[arr.length - 1]).focus(); - } else if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - const newPlatform = this.getAttribute('data-value'); - const currentPlatform = platformSelectHidden.value; - - // Only clear username if platform is actually changing - if (newPlatform !== currentPlatform) { - platformUsername.value = ''; - chrome.storage.local.set({ platformUsername: '' }); - } - - setPlatformDropdown(newPlatform); - customDropdown.classList.remove('open'); - dropdownList.classList.add('hidden'); - dropdownBtn.focus(); - } - }); - }); +}); - // On load, restore platform from storage - chrome.storage.local.get(['platform'], function (result) { - const platform = result.platform || 'github'; - // Just update the UI without clearing username when restoring from storage - if (platform === 'gitlab') { - dropdownSelected.innerHTML = ' GitLab'; - } else { - dropdownSelected.innerHTML = ' GitHub'; - } - platformSelectHidden.value = platform; - updatePlatformUI(platform); - }); +// On load, restore platform from storage +chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + // Just update the UI without clearing username when restoring from storage + if (platform === 'gitlab') { + dropdownSelected.innerHTML = ' GitLab'; + } else { + dropdownSelected.innerHTML = ' GitHub'; + } + platformSelectHidden.value = platform; + updatePlatformUI(platform); +}); @@ -1392,12 +1403,12 @@ document.getElementById('refreshCache').addEventListener('click', async function scrumReport.innerHTML = '

    Cache cleared successfully. Click "Generate Report" to fetch fresh data.

    '; } - if(typeof availableRepos !== 'undefined'){ + if (typeof availableRepos !== 'undefined') { availableRepos = []; } const repoStatus = document.getElementById('repoStatus'); - if(repoStatus){ + if (repoStatus) { repoStatus.textContent = ''; } @@ -1526,7 +1537,7 @@ function toggleRadio(radio) { end: endDateInput.value, isLastWeek: radio.id === 'lastWeekContribution' }); - + triggerRepoFetchIfEnabled(); }); diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 234a0b9..241aeee 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -33,7 +33,7 @@ function allIncluded(outputTarget = 'email') { let scrumSubject = null; let startingDate = ''; let endingDate = ''; - let githubUsername = ''; + let platformUsernameLocal = ''; let githubToken = ''; let projectName = ''; let lastWeekArray = []; @@ -71,7 +71,6 @@ function allIncluded(outputTarget = 'email') { [ 'platform', 'platformUsername', - 'githubUsername', 'githubToken', 'projectName', 'enableToggle', @@ -93,22 +92,22 @@ function allIncluded(outputTarget = 'email') { console.log("[DEBUG] Storage items received:", items); platform = items.platform || 'github'; platformUsername = items.platformUsername || ''; + platformUsernameLocal = platformUsername; console.log(`[DEBUG] platform: ${platform}, platformUsername: ${platformUsername}`); if (outputTarget === 'popup') { - const usernameFromDOM = document.getElementById('githubUsername')?.value; + const usernameFromDOM = document.getElementById('platformUsername')?.value; const projectFromDOM = document.getElementById('projectName')?.value; const tokenFromDOM = document.getElementById('githubToken')?.value; - items.githubUsername = usernameFromDOM || items.githubUsername; + items.platformUsername = usernameFromDOM || items.platformUsername; items.projectName = projectFromDOM || items.projectName; items.githubToken = tokenFromDOM || items.githubToken; chrome.storage.local.set({ - githubUsername: items.githubUsername, + platformUsername: items.platformUsername, projectName: items.projectName, githubToken: items.githubToken }); } - githubUsername = items.githubUsername; projectName = items.projectName; userReason = 'No Blocker at the moment'; @@ -136,9 +135,10 @@ function allIncluded(outputTarget = 'email') { } + // PLATFORM LOGIC FIX if (platform === 'github') { - if (githubUsername) { - console.log("[DEBUG] About to fetch GitHub data for:", githubUsername); + if (platformUsernameLocal) { + console.log("[DEBUG] About to fetch GitHub data for:", platformUsernameLocal); fetchGithubData(); } else { if (outputTarget === 'popup') { @@ -146,7 +146,7 @@ function allIncluded(outputTarget = 'email') { const scrumReport = document.getElementById('scrumReport'); const generateBtn = document.getElementById('generateReport'); if (scrumReport) { - scrumReport.innerHTML = '
    Please enter your GitHub username to generate a report.
    '; + scrumReport.innerHTML = '
    Please enter your username to generate a report.
    '; } if (generateBtn) { generateBtn.innerHTML = ' Generate Report'; @@ -154,76 +154,59 @@ function allIncluded(outputTarget = 'email') { } scrumGenerationInProgress = false; } else { - console.warn('[DEBUG] No GitHub username found in storage'); + console.warn('[DEBUG] No username found in storage'); scrumGenerationInProgress = false; - } return; } } else if (platform === 'gitlab') { if (!gitlabHelper) gitlabHelper = new window.GitLabHelper(); - if (platformUsername) { + if (platformUsernameLocal) { const generateBtn = document.getElementById('generateReport'); if (generateBtn && outputTarget === 'popup') { generateBtn.innerHTML = ' Generating...'; generateBtn.disabled = true; } - - gitlabHelper.fetchGitLabData(platformUsername, startingDate, endingDate) + gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate) .then(data => { - console.log('[SCRUM-DEBUG] Raw GitLab data returned from fetchGitLabData:', data); - const processed = gitlabHelper.processGitLabData(data); - console.log('[SCRUM-DEBUG] Processed GitLab data:', processed); - // Map GitLab issues and MRs to GitHub-like fields for compatibility - function mapGitLabIssue(issue) { - return { - ...issue, - repository_url: issue.web_url ? issue.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, - html_url: issue.web_url, - number: issue.iid, - state: issue.state === "opened" ? "open" : issue.state, // normalize - title: issue.title, - body: issue.description, - pull_request: undefined // not a PR - }; - } - function mapGitLabMR(mr) { + console.log('[SCRUM-HELPER] GitLab data:', data); + // Map GitLab issues and MRs to the expected structure for both popup and email + function mapGitLabItem(item, projects, type) { + const project = projects.find(p => p.id === item.project_id); + const repoName = project ? project.name : 'unknown'; // Use project.name for display return { - ...mr, - repository_url: mr.web_url ? mr.web_url.split('/-/')[0].replace('https://gitlab.com/', 'https://api.gitlab.com/repos/') : undefined, - html_url: mr.web_url, - number: mr.iid, - state: mr.state === "opened" ? "open" : mr.state, // normalize - title: mr.title, - body: mr.description, - pull_request: {}, // mark as PR - author: mr.author // ensure author is present for authored-by check + ...item, + repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, + html_url: type === 'issue' + ? (item.web_url || (project ? `${project.web_url}/-/issues/${item.iid}` : '')) + : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), + number: item.iid, + title: item.title, + state: item.state, + project: repoName, + pull_request: type === 'mr', }; } - githubIssuesData = { items: processed.issues.map(mapGitLabIssue) }; - githubPrsReviewData = { items: processed.mergeRequests.map(mapGitLabMR) }; - githubUserData = processed.user; - console.log('[SCRUM-DEBUG] Passing to processGithubData:', { - githubIssuesData, - githubPrsReviewData, - githubUserData - }); - processGithubData({ - githubIssuesData, - githubPrsReviewData, - githubUserData - }); + const mappedIssues = (data.issues || []).map(issue => mapGitLabItem(issue, data.projects, 'issue')); + const mappedMRs = (data.mergeRequests || data.mrs || []).map(mr => mapGitLabItem(mr, data.projects, 'mr')); + const mappedData = { + githubIssuesData: { items: mappedIssues }, + githubPrsReviewData: { items: mappedMRs }, + githubUserData: data.user || {}, + }; + processGithubData(mappedData); + scrumGenerationInProgress = false; }) .catch(err => { console.error('GitLab fetch failed:', err); if (outputTarget === 'popup') { if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; + generateBtn.innerHTML = ' Generate Report'; generateBtn.disabled = false; } const scrumReport = document.getElementById('scrumReport'); if (scrumReport) { - scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; } } scrumGenerationInProgress = false; @@ -233,433 +216,405 @@ function allIncluded(outputTarget = 'email') { const scrumReport = document.getElementById('scrumReport'); const generateBtn = document.getElementById('generateReport'); if (scrumReport) { - scrumReport.innerHTML = '
    Please enter your GitLab username to generate a report.
    '; + scrumReport.innerHTML = '
    Please enter your username to generate a report.
    '; } if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; + generateBtn.innerHTML = ' Generate Report'; generateBtn.disabled = false; } } scrumGenerationInProgress = false; } } else { - - } - if (items.cacheInput) { - cacheInput = items.cacheInput; - } - - if (!items.showOpenLabel) { - showOpenLabel = false; - pr_unmerged_button = ''; - issue_opened_button = ''; - pr_merged_button = ''; - issue_closed_button = ''; - } - if (items.userReason) { - userReason = items.userReason; - } - if (items.showCommits !== undefined) { - showCommits = items.showCommits; - } else { - showCommits = false; //default value - } - if (items.githubCache) { - githubCache.data = items.githubCache.data; - githubCache.cacheKey = items.githubCache.cacheKey; - githubCache.timestamp = items.githubCache.timestamp; - log('Restored cache from storage'); - } - if (typeof items.orgName !== 'undefined') { - orgName = items.orgName || ''; - console.log('[SCRUM-HELPER] orgName set to:', orgName); - } - if (items.selectedRepos) { - selectedRepos = items.selectedRepos; - } - if (items.useRepoFilter) { - useRepoFilter = items.useRepoFilter; + // Unknown platform + if (outputTarget === 'popup') { + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = '
    Unknown platform selected.
    '; + } + } + scrumGenerationInProgress = false; } }, ); } getChromeData(); - - -function handleLastWeekContributionChange() { - endingDate = getToday(); - startingDate = getLastWeek(); -} -function handleYesterdayContributionChange() { - endingDate = getToday(); - startingDate = getYesterday(); -} -function getLastWeek() { - let today = new Date(); - let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - let lastWeekMonth = lastWeek.getMonth() + 1; - let lastWeekDay = lastWeek.getDate(); - let lastWeekYear = lastWeek.getFullYear(); - let lastWeekDisplayPadded = - ('0000' + lastWeekYear.toString()).slice(-4) + - '-' + - ('00' + lastWeekMonth.toString()).slice(-2) + - '-' + - ('00' + lastWeekDay.toString()).slice(-2); - return lastWeekDisplayPadded; -} -function getYesterday() { - let today = new Date(); - let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - let yesterdayMonth = yesterday.getMonth() + 1; - let yesterdayDay = yesterday.getDate(); - let yesterdayYear = yesterday.getFullYear(); - let yesterdayPadded = - ('0000' + yesterdayYear.toString()).slice(-4) + - '-' + - ('00' + yesterdayMonth.toString()).slice(-2) + - '-' + - ('00' + yesterdayDay.toString()).slice(-2); - return yesterdayPadded; -} -function getToday() { - let today = new Date(); - let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - let WeekMonth = Week.getMonth() + 1; - let WeekDay = Week.getDate(); - let WeekYear = Week.getFullYear(); - let WeekDisplayPadded = - ('0000' + WeekYear.toString()).slice(-4) + - '-' + - ('00' + WeekMonth.toString()).slice(-2) + - '-' + - ('00' + WeekDay.toString()).slice(-2); - return WeekDisplayPadded; -} -// Global cache object -let githubCache = { - data: null, - cacheKey: null, - timestamp: 0, - ttl: 10 * 60 * 1000, // cache valid for 10 mins - fetching: false, - queue: [], - errors: {}, - errorTTL: 60 * 1000, // 1 min error cache - subject: null, - repoData: null, - repoCacheKey: null, - repoTimeStamp: 0, - repoFetching: false, - repoQueue: [], -}; -async function getCacheTTL() { - return new Promise((resolve) => { - chrome.storage.local.get(['cacheInput'], function (result) { - const ttlMinutes = result.cacheInput || 10; - resolve(ttlMinutes * 60 * 1000); - }); - }); -} + function handleLastWeekContributionChange() { + endingDate = getToday(); + startingDate = getLastWeek(); + } + function handleYesterdayContributionChange() { + endingDate = getToday(); + startingDate = getYesterday(); + } + function getLastWeek() { + let today = new Date(); + let lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + let lastWeekMonth = lastWeek.getMonth() + 1; + let lastWeekDay = lastWeek.getDate(); + let lastWeekYear = lastWeek.getFullYear(); + let lastWeekDisplayPadded = + ('0000' + lastWeekYear.toString()).slice(-4) + + '-' + + ('00' + lastWeekMonth.toString()).slice(-2) + + '-' + + ('00' + lastWeekDay.toString()).slice(-2); + return lastWeekDisplayPadded; + } + function getYesterday() { + let today = new Date(); + let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + let yesterdayMonth = yesterday.getMonth() + 1; + let yesterdayDay = yesterday.getDate(); + let yesterdayYear = yesterday.getFullYear(); + let yesterdayPadded = + ('0000' + yesterdayYear.toString()).slice(-4) + + '-' + + ('00' + yesterdayMonth.toString()).slice(-2) + + '-' + + ('00' + yesterdayDay.toString()).slice(-2); + return yesterdayPadded; + } + function getToday() { + let today = new Date(); + let Week = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + let WeekMonth = Week.getMonth() + 1; + let WeekDay = Week.getDate(); + let WeekYear = Week.getFullYear(); + let WeekDisplayPadded = + ('0000' + WeekYear.toString()).slice(-4) + + '-' + + ('00' + WeekMonth.toString()).slice(-2) + + '-' + + ('00' + WeekDay.toString()).slice(-2); + return WeekDisplayPadded; + } + // Global cache object + let githubCache = { + data: null, + cacheKey: null, + timestamp: 0, + ttl: 10 * 60 * 1000, // cache valid for 10 mins + fetching: false, + queue: [], + errors: {}, + errorTTL: 60 * 1000, // 1 min error cache + subject: null, + repoData: null, + repoCacheKey: null, + repoTimeStamp: 0, + repoFetching: false, + repoQueue: [], + }; -function saveToStorage(data, subject = null) { - const cacheData = { - data: data, - cacheKey: githubCache.cacheKey, - timestamp: githubCache.timestamp, - subject: subject, - usedToken: !!githubToken, + async function getCacheTTL() { + return new Promise((resolve) => { + chrome.storage.local.get(['cacheInput'], function (result) { + const ttlMinutes = result.cacheInput || 10; + resolve(ttlMinutes * 60 * 1000); + }); + }); } - log(`Saving data to storage:`, { - cacheKey: githubCache.cacheKey, - timestamp: githubCache.timestamp, - hasSubject: !!subject, - }); - return new Promise((resolve) => { - chrome.storage.local.set({ githubCache: cacheData }, () => { - if (chrome.runtime.lastError) { - logError('Storage save failed: ', chrome.runtime.lastError); - resolve(false); - } else { - log('Cache saved successfuly'); - githubCache.data = data; - githubCache.subject = subject; - resolve(true); - } + + function saveToStorage(data, subject = null) { + const cacheData = { + data: data, + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + subject: subject, + usedToken: !!githubToken, + } + log(`Saving data to storage:`, { + cacheKey: githubCache.cacheKey, + timestamp: githubCache.timestamp, + hasSubject: !!subject, }); - }); -} -function loadFromStorage() { - log('Loading cache from storage'); - return new Promise(async (resolve) => { - const currentTTL = await getCacheTTL(); - chrome.storage.local.get('githubCache', (result) => { - const cache = result.githubCache; - if (!cache) { - log('No cache found in storage'); - resolve(false); - return; - } - const isCacheExpired = (Date.now() - cache.timestamp) > currentTTL; - if (isCacheExpired) { - log('Cached data is expired'); - resolve(false); - return; - } - log('Found valid cache:', { - cacheKey: cache.cacheKey, - age: `${((Date.now() - cache.timestamp) / 1000 / 60).toFixed(1)} minutes`, + return new Promise((resolve) => { + chrome.storage.local.set({ githubCache: cacheData }, () => { + if (chrome.runtime.lastError) { + logError('Storage save failed: ', chrome.runtime.lastError); + resolve(false); + } else { + log('Cache saved successfuly'); + githubCache.data = data; + githubCache.subject = subject; + resolve(true); + } }); - - githubCache.data = cache.data; - githubCache.cacheKey = cache.cacheKey; - githubCache.timestamp = cache.timestamp; - githubCache.subject = cache.subject; - githubCache.usedToken = cache.usedToken || false; - - if (cache.subject && scrumSubject) { - scrumSubject.value = cache.subject; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); - } - resolve(true); }); - }); -} + } -async function fetchGithubData() { - const cacheKey = `${githubUsername}-${startingDate}-${endingDate}-${orgName || 'all'}`; + function loadFromStorage() { + log('Loading cache from storage'); + return new Promise(async (resolve) => { + const currentTTL = await getCacheTTL(); + chrome.storage.local.get('githubCache', (result) => { + const cache = result.githubCache; + if (!cache) { + log('No cache found in storage'); + resolve(false); + return; + } + const isCacheExpired = (Date.now() - cache.timestamp) > currentTTL; + if (isCacheExpired) { + log('Cached data is expired'); + resolve(false); + return; + } + log('Found valid cache:', { + cacheKey: cache.cacheKey, + age: `${((Date.now() - cache.timestamp) / 1000 / 60).toFixed(1)} minutes`, + }); - if (githubCache.fetching || (githubCache.cacheKey === cacheKey && githubCache.data)) { - log('Fetch already in progress or data already fetched. Skipping fetch.'); - return; + githubCache.data = cache.data; + githubCache.cacheKey = cache.cacheKey; + githubCache.timestamp = cache.timestamp; + githubCache.subject = cache.subject; + githubCache.usedToken = cache.usedToken || false; + + if (cache.subject && scrumSubject) { + scrumSubject.value = cache.subject; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + } + resolve(true); + }); + }); } - log('Fetching Github data:', { - username: githubUsername, - startDate: startingDate, - endDate: endingDate, - }); + async function fetchGithubData() { + const cacheKey = `${platformUsernameLocal}-${startingDate}-${endingDate}-${orgName || 'all'}`; - log('CacheKey in cache:', githubCache.cacheKey); - log('Incoming cacheKey:', cacheKey); - log('Has data:', !!githubCache.data); + if (githubCache.fetching || (githubCache.cacheKey === cacheKey && githubCache.data)) { + log('Fetch already in progress or data already fetched. Skipping fetch.'); + return; + } - // Check if we need to load from storage - if (!githubCache.data && !githubCache.fetching) { - await loadFromStorage(); - }; + log('Fetching Github data:', { + username: platformUsernameLocal, + startDate: startingDate, + endDate: endingDate, + }); - const currentTTL = await getCacheTTL(); - githubCache.ttl = currentTTL; - log(`Caching for ${currentTTL / (60 * 1000)} minutes`); + log('CacheKey in cache:', githubCache.cacheKey); + log('Incoming cacheKey:', cacheKey); + log('Has data:', !!githubCache.data); - const now = Date.now(); - const isCacheFresh = (now - githubCache.timestamp) < githubCache.ttl; - const isCacheKeyMatch = githubCache.cacheKey === cacheKey; - const needsToken = !!githubToken; - const cacheUsedToken = !!githubCache.usedToken; + // Check if we need to load from storage + if (!githubCache.data && !githubCache.fetching) { + await loadFromStorage(); + }; - if (githubCache.data && isCacheFresh && isCacheKeyMatch) { - if (needsToken && !cacheUsedToken) { - log('Cache was fetched without token, but user now has a token. Invalidating cache.'); + const currentTTL = await getCacheTTL(); + githubCache.ttl = currentTTL; + log(`Caching for ${currentTTL / (60 * 1000)} minutes`); + + const now = Date.now(); + const isCacheFresh = (now - githubCache.timestamp) < githubCache.ttl; + const isCacheKeyMatch = githubCache.cacheKey === cacheKey; + const needsToken = !!githubToken; + const cacheUsedToken = !!githubCache.usedToken; + + if (githubCache.data && isCacheFresh && isCacheKeyMatch) { + if (needsToken && !cacheUsedToken) { + log('Cache was fetched without token, but user now has a token. Invalidating cache.'); + githubCache.data = null; + } else { + log('Using cached data - cache is fresh and key matches'); + processGithubData(githubCache.data); + return Promise.resolve(); + } + } + // if cache key does not match our cache is stale, fetch new data + if (!isCacheKeyMatch) { + log('Cache key mismatch - fetching new Data'); githubCache.data = null; - } else { - log('Using cached data - cache is fresh and key matches'); - processGithubData(githubCache.data); - return Promise.resolve(); + } else if (!isCacheFresh) { + log('Cache is stale - fetching new data'); } - } - // if cache key does not match our cache is stale, fetch new data - if (!isCacheKeyMatch) { - log('Cache key mismatch - fetching new Data'); - githubCache.data = null; - } else if (!isCacheFresh) { - log('Cache is stale - fetching new data'); - } - // if fetching is in progress, queue the calls and return a promise resolved when done - if (githubCache.fetching) { - log('Fetch in progress, queuing requests'); - return new Promise((resolve, reject) => { - githubCache.queue.push({ resolve, reject }); - }); - } + // if fetching is in progress, queue the calls and return a promise resolved when done + if (githubCache.fetching) { + log('Fetch in progress, queuing requests'); + return new Promise((resolve, reject) => { + githubCache.queue.push({ resolve, reject }); + }); + } - githubCache.fetching = true; - githubCache.cacheKey = cacheKey; - githubCache.usedToken = !!githubToken; + githubCache.fetching = true; + githubCache.cacheKey = cacheKey; + githubCache.usedToken = !!githubToken; - const headers = { - 'Accept': 'application/vnd.github.v3+json', - }; + const headers = { + 'Accept': 'application/vnd.github.v3+json', + }; - if (githubToken) { - log('Making authenticated requests.'); - headers['Authorization'] = `token ${githubToken}`; + if (githubToken) { + log('Making authenticated requests.'); + headers['Authorization'] = `token ${githubToken}`; - } else { - log('Making public requests'); - } + } else { + log('Making public requests'); + } - console.log('[SCRUM-HELPER] orgName before API query:', orgName); - console.log('[SCRUM-HELPER] orgName type:', typeof orgName); - console.log('[SCRUM-HELPER] orgName length:', orgName ? orgName.length : 0); - let orgPart = orgName && orgName.trim() ? `+org%3A${orgName}` : ''; - console.log('[SCRUM-HELPER] orgPart for API:', orgPart); - console.log('[SCRUM-HELPER] orgPart length:', orgPart.length); + console.log('[SCRUM-HELPER] orgName before API query:', orgName); + console.log('[SCRUM-HELPER] orgName type:', typeof orgName); + console.log('[SCRUM-HELPER] orgName length:', orgName ? orgName.length : 0); + let orgPart = orgName && orgName.trim() ? `+org%3A${orgName}` : ''; + console.log('[SCRUM-HELPER] orgPart for API:', orgPart); + console.log('[SCRUM-HELPER] orgPart length:', orgPart.length); - let issueUrl, prUrl, userUrl; + let issueUrl, prUrl, userUrl; - if (useRepoFilter && selectedRepos && selectedRepos.length > 0) { - log('Using repo filter for api calls:', selectedRepos); + if (useRepoFilter && selectedRepos && selectedRepos.length > 0) { + log('Using repo filter for api calls:', selectedRepos); - try { - await fetchReposIfNeeded(); - } catch (err) { - logError('Failed to fetch repo data for filtering:', err); - } + try { + await fetchReposIfNeeded(); + } catch (err) { + logError('Failed to fetch repo data for filtering:', err); + } - const repoQueries = selectedRepos - .filter(repo => repo !== null) - .map(repo => { - if (typeof repo === 'object' && repo.fullName) { - // FIXED: Remove leading slash if present - const cleanName = repo.fullName.startsWith('/') ? repo.fullName.substring(1) : repo.fullName; - return `repo:${cleanName}`; - } else if (repo.includes('/')) { - // FIXED: Remove leading slash if present - const cleanName = repo.startsWith('/') ? repo.substring(1) : repo; - return `repo:${cleanName}`; - } else { - const fullRepoInfo = githubCache.repoData?.find(r => r.name === repo); - if (fullRepoInfo && fullRepoInfo.fullName) { - return `repo:${fullRepoInfo.fullName}`; + const repoQueries = selectedRepos + .filter(repo => repo !== null) + .map(repo => { + if (typeof repo === 'object' && repo.fullName) { + // FIXED: Remove leading slash if present + const cleanName = repo.fullName.startsWith('/') ? repo.fullName.substring(1) : repo.fullName; + return `repo:${cleanName}`; + } else if (repo.includes('/')) { + // FIXED: Remove leading slash if present + const cleanName = repo.startsWith('/') ? repo.substring(1) : repo; + return `repo:${cleanName}`; + } else { + const fullRepoInfo = githubCache.repoData?.find(r => r.name === repo); + if (fullRepoInfo && fullRepoInfo.fullName) { + return `repo:${fullRepoInfo.fullName}`; + } + logError(`Missing owner for repo ${repo} - search may fail`); + return `repo:${repo}`; } - logError(`Missing owner for repo ${repo} - search may fail`); - return `repo:${repo}`; - } - }).join('+'); - - const orgQuery = orgPart ? `+${orgPart}` : ''; - issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - userUrl = `https://api.github.com/users/${githubUsername}`; - log('Repository-filtered URLs:', { issueUrl, prUrl }); - } else { - loadFromStorage('Using org wide search'); - const orgQuery = orgPart ? `+${orgPart}` : ''; - issueUrl = `https://api.github.com/search/issues?q=author%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - prUrl = `https://api.github.com/search/issues?q=commenter%3A${githubUsername}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; - userUrl = `https://api.github.com/users/${githubUsername}`; - } + }).join('+'); - try { - // throttling 500ms to avoid burst - await new Promise(res => setTimeout(res, 500)); + const orgQuery = orgPart ? `+${orgPart}` : ''; + issueUrl = `https://api.github.com/search/issues?q=author%3A${platformUsernameLocal}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + prUrl = `https://api.github.com/search/issues?q=commenter%3A${platformUsernameLocal}+${repoQueries}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + userUrl = `https://api.github.com/users/${platformUsernameLocal}`; + log('Repository-filtered URLs:', { issueUrl, prUrl }); + } else { + loadFromStorage('Using org wide search'); + const orgQuery = orgPart ? `+${orgPart}` : ''; + issueUrl = `https://api.github.com/search/issues?q=author%3A${platformUsernameLocal}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + prUrl = `https://api.github.com/search/issues?q=commenter%3A${platformUsernameLocal}${orgQuery}+updated%3A${startingDate}..${endingDate}&per_page=100`; + userUrl = `https://api.github.com/users/${platformUsernameLocal}`; + } - const [issuesRes, prRes, userRes] = await Promise.all([ - fetch(issueUrl, { headers }), - fetch(prUrl, { headers }), - fetch(userUrl, { headers }), - ]); + try { + // throttling 500ms to avoid burst + await new Promise(res => setTimeout(res, 500)); - if (issuesRes.status === 401 || prRes.status === 401 || userRes.status === 401 || - issuesRes.status === 403 || prRes.status === 403 || userRes.status === 403) { - showInvalidTokenMessage(); - return; - } + const [issuesRes, prRes, userRes] = await Promise.all([ + fetch(issueUrl, { headers }), + fetch(prUrl, { headers }), + fetch(userUrl, { headers }), + ]); - if (issuesRes.status === 404 || prRes.status === 404) { - if (outputTarget === 'popup') { - Materialize.toast && Materialize.toast('Organization not found on GitHub', 3000); + if (issuesRes.status === 401 || prRes.status === 401 || userRes.status === 401 || + issuesRes.status === 403 || prRes.status === 403 || userRes.status === 403) { + showInvalidTokenMessage(); + return; } - throw new Error('Organization not found'); - } - if (!issuesRes.ok) throw new Error(`Error fetching Github issues: ${issuesRes.status} ${issuesRes.statusText}`); - if (!prRes.ok) throw new Error(`Error fetching Github PR review data: ${prRes.status} ${prRes.statusText}`); - if (!userRes.ok) throw new Error(`Error fetching Github userdata: ${userRes.status} ${userRes.statusText}`); - - githubIssuesData = await issuesRes.json(); - githubPrsReviewData = await prRes.json(); - githubUserData = await userRes.json(); - - if (githubIssuesData && githubIssuesData.items) { - log('Fetched githubIssuesData:', githubIssuesData.items.length, 'items'); - // Collect open PRs - const openPRs = githubIssuesData.items.filter( - item => item.pull_request && item.state === 'open' - ); - log('Open PRs for commit fetching:', openPRs.map(pr => pr.number)); - // Fetch commits for open PRs (batch) - if (openPRs.length && githubToken) { - const commitMap = await fetchCommitsForOpenPRs(openPRs, githubToken, startingDate, endingDate); - log('Commit map returned from fetchCommitsForOpenPRs:', commitMap); - // Attach commits to PR objects - openPRs.forEach(pr => { - pr._allCommits = commitMap[pr.number] || []; - log(`Attached ${pr._allCommits.length} commits to PR #${pr.number}`); - }); + if (issuesRes.status === 404 || prRes.status === 404) { + if (outputTarget === 'popup') { + Materialize.toast && Materialize.toast('Organization not found on GitHub', 3000); + } + throw new Error('Organization not found'); } - } - // Cache the data - githubCache.data = { githubIssuesData, githubPrsReviewData, githubUserData }; - githubCache.timestamp = Date.now(); + if (!issuesRes.ok) throw new Error(`Error fetching Github issues: ${issuesRes.status} ${issuesRes.statusText}`); + if (!prRes.ok) throw new Error(`Error fetching Github PR review data: ${prRes.status} ${prRes.statusText}`); + if (!userRes.ok) throw new Error(`Error fetching Github userdata: ${userRes.status} ${userRes.statusText}`); + + githubIssuesData = await issuesRes.json(); + githubPrsReviewData = await prRes.json(); + githubUserData = await userRes.json(); + + if (githubIssuesData && githubIssuesData.items) { + log('Fetched githubIssuesData:', githubIssuesData.items.length, 'items'); + // Collect open PRs + const openPRs = githubIssuesData.items.filter( + item => item.pull_request && item.state === 'open' + ); + log('Open PRs for commit fetching:', openPRs.map(pr => pr.number)); + // Fetch commits for open PRs (batch) + if (openPRs.length && githubToken) { + const commitMap = await fetchCommitsForOpenPRs(openPRs, githubToken, startingDate, endingDate); + log('Commit map returned from fetchCommitsForOpenPRs:', commitMap); + // Attach commits to PR objects + openPRs.forEach(pr => { + pr._allCommits = commitMap[pr.number] || []; + log(`Attached ${pr._allCommits.length} commits to PR #${pr.number}`); + }); + } + } - await saveToStorage(githubCache.data); - processGithubData(githubCache.data); + // Cache the data + githubCache.data = { githubIssuesData, githubPrsReviewData, githubUserData }; + githubCache.timestamp = Date.now(); - githubCache.queue.forEach(({ resolve }) => resolve()); - githubCache.queue = []; - } catch (err) { - logError('Fetch Failed:', err); - // Reject queued calls on error - githubCache.queue.forEach(({ reject }) => reject(err)); - githubCache.queue = []; - githubCache.fetching = false; + await saveToStorage(githubCache.data); + processGithubData(githubCache.data); - if (outputTarget === 'popup') { - const generateBtn = document.getElementById('generateReport'); - if (scrumReport) { - let errorMsg = 'An error occurred while generating the report.'; - if (err) { - if (typeof err === 'string') errorMsg = err; - else if (err.message) errorMsg = err.message; - else errorMsg = JSON.stringify(err) + githubCache.queue.forEach(({ resolve }) => resolve()); + githubCache.queue = []; + } catch (err) { + logError('Fetch Failed:', err); + // Reject queued calls on error + githubCache.queue.forEach(({ reject }) => reject(err)); + githubCache.queue = []; + githubCache.fetching = false; + + if (outputTarget === 'popup') { + const generateBtn = document.getElementById('generateReport'); + if (scrumReport) { + let errorMsg = 'An error occurred while generating the report.'; + if (err) { + if (typeof err === 'string') errorMsg = err; + else if (err.message) errorMsg = err.message; + else errorMsg = JSON.stringify(err) + } + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while generating the report.'}
    `; + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; } - scrumReport.innerHTML = `
    ${err.message || 'An error occurred while generating the report.'}
    `; - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; - } - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; } + scrumGenerationInProgress = false; + throw err; + } finally { + githubCache.fetching = false; } - scrumGenerationInProgress = false; - throw err; - } finally { - githubCache.fetching = false; } -} -async function fetchCommitsForOpenPRs(prs, githubToken, startDate, endDate) { - log('fetchCommitsForOpenPRs called with PRs:', prs.map(pr => pr.number), 'startDate:', startDate, 'endDate:', endDate); - if (!prs.length) return {}; - const since = new Date(startDate).toISOString(); - const until = new Date(endDate + 'T23:59:59').toISOString(); - let queries = prs.map((pr, idx) => { - const repoParts = pr.repository_url.split('/'); - const owner = repoParts[repoParts.length - 2]; - const repo = repoParts[repoParts.length - 1]; - return ` + async function fetchCommitsForOpenPRs(prs, githubToken, startDate, endDate) { + log('fetchCommitsForOpenPRs called with PRs:', prs.map(pr => pr.number), 'startDate:', startDate, 'endDate:', endDate); + if (!prs.length) return {}; + const since = new Date(startDate).toISOString(); + const until = new Date(endDate + 'T23:59:59').toISOString(); + let queries = prs.map((pr, idx) => { + const repoParts = pr.repository_url.split('/'); + const owner = repoParts[repoParts.length - 2]; + const repo = repoParts[repoParts.length - 1]; + return ` pr${idx}: repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${pr.number}) { commits(first: 100) { @@ -678,886 +633,883 @@ async function fetchCommitsForOpenPRs(prs, githubToken, startDate, endDate) { } }`; - }).join('\n'); - const query = `query { ${queries} }`; - log('GraphQL query for commits:', query); - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(githubToken ? { Authorization: `bearer ${githubToken}` } : {}) - }, - body: JSON.stringify({ query }) - }); - log('fetchCommitsForOpenPRs response status:', res.status); - const data = await res.json(); - log('fetchCommitsForOpenPRs response data:', data); - let commitMap = {}; - prs.forEach((pr, idx) => { - const prData = data.data && data.data[`pr${idx}`] && data.data[`pr${idx}`].pullRequest; - if (prData && prData.commits && prData.commits.nodes) { - const allCommits = prData.commits.nodes.map(n => n.commit); - log(`PR #${pr.number} allCommits:`, allCommits); - const filteredCommits = allCommits.filter(commit => { - const commitDate = new Date(commit.committedDate); - const sinceDate = new Date(since); - const untilDate = new Date(until); - return commitDate >= sinceDate && commitDate <= untilDate; - }); - log(`PR #${pr.number} filteredCommits:`, filteredCommits); - commitMap[pr.number] = filteredCommits; - } else { - log(`No commits found for PR #${pr.number}`); + }).join('\n'); + const query = `query { ${queries} }`; + log('GraphQL query for commits:', query); + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(githubToken ? { Authorization: `bearer ${githubToken}` } : {}) + }, + body: JSON.stringify({ query }) + }); + log('fetchCommitsForOpenPRs response status:', res.status); + const data = await res.json(); + log('fetchCommitsForOpenPRs response data:', data); + let commitMap = {}; + prs.forEach((pr, idx) => { + const prData = data.data && data.data[`pr${idx}`] && data.data[`pr${idx}`].pullRequest; + if (prData && prData.commits && prData.commits.nodes) { + const allCommits = prData.commits.nodes.map(n => n.commit); + log(`PR #${pr.number} allCommits:`, allCommits); + const filteredCommits = allCommits.filter(commit => { + const commitDate = new Date(commit.committedDate); + const sinceDate = new Date(since); + const untilDate = new Date(until); + return commitDate >= sinceDate && commitDate <= untilDate; + }); + log(`PR #${pr.number} filteredCommits:`, filteredCommits); + commitMap[pr.number] = filteredCommits; + } else { + log(`No commits found for PR #${pr.number}`); + } + }); + return commitMap; + } + + async function fetchReposIfNeeded() { + if (!useRepoFilter) { + log('Repo fiter disabled, skipping fetch'); + return []; } - }); - return commitMap; -} + const repoCacheKey = `repos-${platformUsernameLocal}-${orgName}-${startingDate}-${endingDate}`; -async function fetchReposIfNeeded() { - if (!useRepoFilter) { - log('Repo fiter disabled, skipping fetch'); - return []; - } - const repoCacheKey = `repos-${githubUsername}-${orgName}-${startingDate}-${endingDate}`; + const now = Date.now(); + const isRepoCacheFresh = (now - githubCache.repoTimeStamp) < githubCache.ttl; + const isRepoCacheKeyMatch = githubCache.repoCacheKey === repoCacheKey; - const now = Date.now(); - const isRepoCacheFresh = (now - githubCache.repoTimeStamp) < githubCache.ttl; - const isRepoCacheKeyMatch = githubCache.repoCacheKey === repoCacheKey; + if (githubCache.repoData && isRepoCacheFresh && isRepoCacheKeyMatch) { + log('Using cached repo data'); + return githubCache.repoData; + } - if (githubCache.repoData && isRepoCacheFresh && isRepoCacheKeyMatch) { - log('Using cached repo data'); - return githubCache.repoData; - } + if (githubCache.repoFetching) { + log('Repo fetch is in progress, queuing request'); + return new Promise((resolve, reject) => { + githubCache.repoQueue.push({ resolve, reject }); + }); + } - if (githubCache.repoFetching) { - log('Repo fetch is in progress, queuing request'); - return new Promise((resolve, reject) => { - githubCache.repoQueue.push({ resolve, reject }); - }); - } + githubCache.repoFetching = true; + githubCache.repoCacheKey = repoCacheKey; - githubCache.repoFetching = true; - githubCache.repoCacheKey = repoCacheKey; + try { + log('Fetching repos automatically'); + const repos = await fetchUserRepositories(platformUsernameLocal, githubToken, orgName); - try { - log('Fetching repos automatically'); - const repos = await fetchUserRepositories(githubUsername, githubToken, orgName); + githubCache.repoData = repos; + githubCache.repoTimeStamp = now; - githubCache.repoData = repos; - githubCache.repoTimeStamp = now; + chrome.storage.local.set({ + repoCache: { + data: repos, + cacheKey: repoCacheKey, + timestamp: now + } + }); - chrome.storage.local.set({ - repoCache: { - data: repos, - cacheKey: repoCacheKey, - timestamp: now - } - }); + githubCache.repoQueue.forEach(({ resolve }) => resolve(repos)); + githubCache.repoQueue = []; - githubCache.repoQueue.forEach(({ resolve }) => resolve(repos)); - githubCache.repoQueue = []; + log(`Successfuly cached ${repos.length} repositories`); + return repos; + } catch (err) { + logError('Failed to fetch reppos:', err); + githubCache.repoQueue.forEach(({ reject }) => reject(err)); + githubCache.repoQueue = []; - log(`Successfuly cached ${repos.length} repositories`); - return repos; - } catch (err) { - logError('Failed to fetch reppos:', err); - githubCache.repoQueue.forEach(({ reject }) => reject(err)); - githubCache.repoQueue = []; + throw err; + } finally { + githubCache.repoFetching = false; + } + } - throw err; - } finally { - githubCache.repoFetching = false; + async function verifyCacheStatus() { + log('Cache Status: ', { + hasCachedData: !!githubCache.data, + cacheAge: githubCache.timestamp ? `${((Date.now() - githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : `no cache`, + cacheKey: githubCache.cacheKey, + isFetching: githubCache.fetching, + queueLength: githubCache.queue.length + }); + const storageData = await new Promise(resolve => { + chrome.storage.local.get('githubCache', resolve); + }); + log('Storage Status:', { + hasStoredData: !!storageData.githubCache, + storedCacheKey: storageData.githubCache?.cacheKey, + storageAge: storageData.githubCache?.timestamp ? + `${((Date.now() - storageData.githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : + 'no data' + }); } -} + verifyCacheStatus(); -async function verifyCacheStatus() { - log('Cache Status: ', { - hasCachedData: !!githubCache.data, - cacheAge: githubCache.timestamp ? `${((Date.now() - githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : `no cache`, - cacheKey: githubCache.cacheKey, - isFetching: githubCache.fetching, - queueLength: githubCache.queue.length - }); - const storageData = await new Promise(resolve => { - chrome.storage.local.get('githubCache', resolve); - }); - log('Storage Status:', { - hasStoredData: !!storageData.githubCache, - storedCacheKey: storageData.githubCache?.cacheKey, - storageAge: storageData.githubCache?.timestamp ? - `${((Date.now() - storageData.githubCache.timestamp) / 1000 / 60).toFixed(1)} minutes` : - 'no data' - }); -} -verifyCacheStatus(); - -function showInvalidTokenMessage() { - if (outputTarget === 'popup') { - const reportDiv = document.getElementById('scrumReport'); - if (reportDiv) { - reportDiv.innerHTML = '
    Invalid or expired GitHub token. Please check your token in the settings and try again.
    '; - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; + function showInvalidTokenMessage() { + if (outputTarget === 'popup') { + const reportDiv = document.getElementById('scrumReport'); + if (reportDiv) { + reportDiv.innerHTML = '
    Invalid or expired GitHub token. Please check your token in the settings and try again.
    '; + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + } else { + alert('Invalid or expired GitHub token. Please check your token in the extension popup and try again.'); } - } else { - alert('Invalid or expired GitHub token. Please check your token in the extension popup and try again.'); } } -} - -async function processGithubData(data) { - githubIssuesData = data.githubIssuesData; - githubPrsReviewData = data.githubPrsReviewData; - githubUserData = data.githubUserData; - log('[DEBUG] GitHub data set:', { - issues: githubIssuesData?.items?.length || 0, - prs: githubPrsReviewData?.items?.length || 0, - user: githubUserData?.login, - filtered: useRepoFilter - }); + async function processGithubData(data) { - lastWeekArray = []; - nextWeekArray = []; - reviewedPrsArray = []; - githubPrsReviewDataProcessed = {}; - issuesDataProcessed = false; - prsReviewDataProcessed = false; + githubIssuesData = data.githubIssuesData; + githubPrsReviewData = data.githubPrsReviewData; + githubUserData = data.githubUserData; + log('[DEBUG] GitHub data set:', { + issues: githubIssuesData?.items?.length || 0, + prs: githubPrsReviewData?.items?.length || 0, + user: githubUserData?.login, + filtered: useRepoFilter + }); - // Update subject + lastWeekArray = []; + nextWeekArray = []; + reviewedPrsArray = []; + githubPrsReviewDataProcessed = {}; + issuesDataProcessed = false; + prsReviewDataProcessed = false; - if (!githubCache.subject && scrumSubject) { - scrumSubjectLoaded(); - } - log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); + // Update subject - if (platform === 'github') { - await writeGithubIssuesPrs(githubIssuesData?.items || []); - } else if (platform === 'gitlab') { - await writeGithubIssuesPrs(githubIssuesData?.items || []); - await writeGithubIssuesPrs(githubPrsReviewData?.items || []); - } + if (!githubCache.subject && scrumSubject) { + scrumSubjectLoaded(); + } + log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); + if (platform === 'github') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + } else if (platform === 'gitlab') { + await writeGithubIssuesPrs(githubIssuesData?.items || []); + await writeGithubIssuesPrs(githubPrsReviewData?.items || []); + } - await writeGithubPrsReviews(); - log('[DEBUG] Both data processing functions completed, generating scrum body'); - writeScrumBody(); -} -function formatDate(dateString) { - const date = new Date(dateString); - const options = { day: '2-digit', month: 'short', year: 'numeric' }; - return date.toLocaleDateString('en-US', options); -} + await writeGithubPrsReviews(); + log('[DEBUG] Both data processing functions completed, generating scrum body'); + writeScrumBody(); + } -function writeScrumBody() { - if (!enableToggle) { - scrumGenerationInProgress = false; - return; + function formatDate(dateString) { + const date = new Date(dateString); + const options = { day: '2-digit', month: 'short', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); } - let lastWeekUl = '
      '; - for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; - for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; - lastWeekUl += '
    '; + function writeScrumBody() { + if (!enableToggle) { + scrumGenerationInProgress = false; + return; + } - let nextWeekUl = '
      '; - for (let i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; - nextWeekUl += '
    '; + let lastWeekUl = '
      '; + for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; + for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; + lastWeekUl += '
    '; - let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); - let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + let nextWeekUl = '
      '; + for (let i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; + nextWeekUl += '
    '; - let content; - if (lastWeekContribution == true || yesterdayContribution == true) { - content = `1. What did I do ${weekOrDay}?
    + let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); + let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + + let content; + if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } else { - content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    + } else { + content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; - } + } - if (outputTarget === 'popup') { - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - log("Found popup div, updating content"); - scrumReport.innerHTML = content; + if (outputTarget === 'popup') { + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + log("Found popup div, updating content"); + scrumReport.innerHTML = content; - const generateBtn = document.getElementById('generateReport'); - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; + const generateBtn = document.getElementById('generateReport'); + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + } else { + logError('Scrum report div not found in popup'); } - } else { - logError('Scrum report div not found in popup'); - } - scrumGenerationInProgress = false; - } else if (outputTarget === 'email') { - if (hasInjectedContent) { scrumGenerationInProgress = false; - return; - } - - const observer = new MutationObserver((mutations, obs) => { - if (!window.emailClientAdapter) { - obs.disconnect(); + } else if (outputTarget === 'email') { + if (hasInjectedContent) { + scrumGenerationInProgress = false; return; } - if (window.emailClientAdapter.isNewConversation()) { - const elements = window.emailClientAdapter.getEditorElements(); - if (elements && elements.body) { + + const observer = new MutationObserver((mutations, obs) => { + if (!window.emailClientAdapter) { obs.disconnect(); - log('MutationObserver found the editor body. Injecting scrum content.'); - window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); - hasInjectedContent = true; - scrumGenerationInProgress = false; + return; } - } - }); + if (window.emailClientAdapter.isNewConversation()) { + const elements = window.emailClientAdapter.getEditorElements(); + if (elements && elements.body) { + obs.disconnect(); + log('MutationObserver found the editor body. Injecting scrum content.'); + window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); + hasInjectedContent = true; + scrumGenerationInProgress = false; + } + } + }); - observer.observe(document.body, { - childList: true, - subtree: true - }); + observer.observe(document.body, { + childList: true, + subtree: true + }); - setTimeout(() => { - observer.disconnect(); - if (!hasInjectedContent && scrumGenerationInProgress) { - logError('Injection timed out after 30 seconds. The compose window might not have loaded.'); - scrumGenerationInProgress = false; + setTimeout(() => { + observer.disconnect(); + if (!hasInjectedContent && scrumGenerationInProgress) { + logError('Injection timed out after 30 seconds. The compose window might not have loaded.'); + scrumGenerationInProgress = false; + } + }, 30000); + } + } + + //load initial scrum subject + function scrumSubjectLoaded() { + try { + if (!enableToggle) return; + if (!scrumSubject) { + console.error('Subject element not found'); + return; } - }, 30000); + setTimeout(() => { + let name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; + let project = projectName || ''; + let curDate = new Date(); + let year = curDate.getFullYear().toString(); + let date = curDate.getDate(); + let month = curDate.getMonth(); + month++; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + let dateCode = year.toString() + month.toString() + date.toString(); + + const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; + log('Generated subject:', subject); + githubCache.subject = subject; + saveToStorage(githubCache.data, subject); + + if (scrumSubject && scrumSubject.value !== subject) { + scrumSubject.value = subject; + scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + } catch (err) { + console.error('Error while setting subject: ', err); + } } -} -//load initial scrum subject -function scrumSubjectLoaded() { - try { - if (!enableToggle) return; - if (!scrumSubject) { - console.error('Subject element not found'); + function writeGithubPrsReviews() { + let items = githubPrsReviewData.items; + log('Processing PR reviews:', { + hasItems: !!items, + itemCount: items?.length, + firstItem: items?.[0] + }); + if (!items) { + logError('No Github PR review data available'); return; } - setTimeout(() => { - let name = githubUserData?.name || githubUserData?.username || githubUsername || platformUsername; - let project = projectName || ''; - let curDate = new Date(); - let year = curDate.getFullYear().toString(); - let date = curDate.getDate(); - let month = curDate.getMonth(); - month++; - if (month < 10) month = '0' + month; - if (date < 10) date = '0' + date; - let dateCode = year.toString() + month.toString() + date.toString(); - - const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; - log('Generated subject:', subject); - githubCache.subject = subject; - saveToStorage(githubCache.data, subject); - - if (scrumSubject && scrumSubject.value !== subject) { - scrumSubject.value = subject; - scrumSubject.dispatchEvent(new Event('input', { bubbles: true })); + reviewedPrsArray = []; + githubPrsReviewDataProcessed = {}; + let i; + for (i = 0; i < items.length; i++) { + let item = items[i]; + // For GitHub: item.user.login, for GitLab: item.author?.username + let isAuthoredByUser = false; + if (platform === 'github') { + isAuthoredByUser = item.user && item.user.login === platformUsernameLocal; + } else if (platform === 'gitlab') { + isAuthoredByUser = item.author && (item.author.username === platformUsername); } - }); - } catch (err) { - console.error('Error while setting subject: ', err); - } -} -function writeGithubPrsReviews() { - let items = githubPrsReviewData.items; - log('Processing PR reviews:', { - hasItems: !!items, - itemCount: items?.length, - firstItem: items?.[0] - }); - if (!items) { - logError('No Github PR review data available'); - return; - } - reviewedPrsArray = []; - githubPrsReviewDataProcessed = {}; - let i; - for (i = 0; i < items.length; i++) { - let item = items[i]; - // For GitHub: item.user.login, for GitLab: item.author?.username - let isAuthoredByUser = false; - if (platform === 'github') { - isAuthoredByUser = item.user && item.user.login === githubUsername; - } else if (platform === 'gitlab') { - isAuthoredByUser = item.author && (item.author.username === platformUsername); - } - - if (isAuthoredByUser || !item.pull_request) continue; - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; - } - let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - let title = item.title; - let number = item.number; - let html_url = item.html_url; - if (!githubPrsReviewDataProcessed[project]) { - // first pr in this repo - githubPrsReviewDataProcessed[project] = []; - } - let obj = { - number: number, - html_url: html_url, - title: title, - state: item.state, - }; - githubPrsReviewDataProcessed[project].push(obj); - } - for (let repo in githubPrsReviewDataProcessed) { - let repoLi = - '
  • (' + - repo + - ') - Reviewed '; - if (githubPrsReviewDataProcessed[repo].length > 1) repoLi += 'PRs - '; - else { - repoLi += 'PR - '; + if (isAuthoredByUser || !item.pull_request) continue; + let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } + let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); + let title = item.title; + let number = item.number; + let html_url = item.html_url; + if (!githubPrsReviewDataProcessed[project]) { + // first pr in this repo + githubPrsReviewDataProcessed[project] = []; + } + let obj = { + number: number, + html_url: html_url, + title: title, + state: item.state, + }; + githubPrsReviewDataProcessed[project].push(obj); } - if (githubPrsReviewDataProcessed[repo].length <= 1) { - for (let pr in githubPrsReviewDataProcessed[repo]) { - let pr_arr = githubPrsReviewDataProcessed[repo][pr]; - let prText = ''; - prText += - "#" + pr_arr.number + ' (' + pr_arr.title + ') '; - if (pr_arr.state === 'open') prText += issue_opened_button; - // Do not show closed label for reviewed PRs - prText += '  '; - repoLi += prText; + for (let repo in githubPrsReviewDataProcessed) { + let repoLi = + '
  • (' + + repo + + ') - Reviewed '; + if (githubPrsReviewDataProcessed[repo].length > 1) repoLi += 'PRs - '; + else { + repoLi += 'PR - '; } - } else { - repoLi += '
      '; - for (let pr1 in githubPrsReviewDataProcessed[repo]) { - let pr_arr1 = githubPrsReviewDataProcessed[repo][pr1]; - let prText1 = ''; - prText1 += - "
    • #" + - pr_arr1.number + - ' (' + - pr_arr1.title + - ') '; - if (pr_arr1.state === 'open') prText1 += issue_opened_button; - // Do not show closed label for reviewed PRs - prText1 += '  
    • '; - repoLi += prText1; + if (githubPrsReviewDataProcessed[repo].length <= 1) { + for (let pr in githubPrsReviewDataProcessed[repo]) { + let pr_arr = githubPrsReviewDataProcessed[repo][pr]; + let prText = ''; + prText += + "#" + pr_arr.number + ' (' + pr_arr.title + ') '; + if (pr_arr.state === 'open') prText += issue_opened_button; + // Do not show closed label for reviewed PRs + prText += '  '; + repoLi += prText; + } + } else { + repoLi += '
        '; + for (let pr1 in githubPrsReviewDataProcessed[repo]) { + let pr_arr1 = githubPrsReviewDataProcessed[repo][pr1]; + let prText1 = ''; + prText1 += + "
      • #" + + pr_arr1.number + + ' (' + + pr_arr1.title + + ') '; + if (pr_arr1.state === 'open') prText1 += issue_opened_button; + // Do not show closed label for reviewed PRs + prText1 += '  
      • '; + repoLi += prText1; + } + repoLi += '
      '; } - repoLi += '
    '; + repoLi += '
  • '; + reviewedPrsArray.push(repoLi); } - repoLi += ''; - reviewedPrsArray.push(repoLi); + prsReviewDataProcessed = true; } - prsReviewDataProcessed = true; -} -function triggerScrumGeneration() { - if (issuesDataProcessed && prsReviewDataProcessed) { - log('Both data sets processed, generating scrum body.'); - writeScrumBody(); - } else { - log('Waiting for all data to be processed before generating scrum.', { - issues: issuesDataProcessed, - reviews: prsReviewDataProcessed, - }); + function triggerScrumGeneration() { + if (issuesDataProcessed && prsReviewDataProcessed) { + log('Both data sets processed, generating scrum body.'); + writeScrumBody(); + } else { + log('Waiting for all data to be processed before generating scrum.', { + issues: issuesDataProcessed, + reviews: prsReviewDataProcessed, + }); + } } -} -function getDaysBetween(start, end) { - const d1 = new Date(start); - const d2 = new Date(end); - return Math.ceil((d2 - d1) / (1000 * 60 * 60 * 24)); -} + function getDaysBetween(start, end) { + const d1 = new Date(start); + const d2 = new Date(end); + return Math.ceil((d2 - d1) / (1000 * 60 * 60 * 24)); + } -let sessionMergedStatusCache = {}; + let sessionMergedStatusCache = {}; -async function fetchPrMergedStatusREST(owner, repo, number, headers) { - const cacheKey = `${owner}/${repo}#${number}`; - if (sessionMergedStatusCache[cacheKey] !== undefined) { - return sessionMergedStatusCache[cacheKey]; - } - const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`; - try { - const res = await fetch(url, { headers }); - if (!res.ok) return null; - const data = await res.json(); - const merged = !!data.merged_at; - sessionMergedStatusCache[cacheKey] = merged; - return merged; - } catch (e) { - return null; + async function fetchPrMergedStatusREST(owner, repo, number, headers) { + const cacheKey = `${owner}/${repo}#${number}`; + if (sessionMergedStatusCache[cacheKey] !== undefined) { + return sessionMergedStatusCache[cacheKey]; + } + const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`; + try { + const res = await fetch(url, { headers }); + if (!res.ok) return null; + const data = await res.json(); + const merged = !!data.merged_at; + sessionMergedStatusCache[cacheKey] = merged; + return merged; + } catch (e) { + return null; + } } -} -async function writeGithubIssuesPrs(items) { - log('writeGithubIssuesPrs called'); + async function writeGithubIssuesPrs(items) { + log('writeGithubIssuesPrs called'); - if (!items) { - logError('No items to process for writeGithubIssuesPrs'); - return; - } - if (!items.length) { - logError('No items to process for writeGithubIssuesPrs'); - return; - } - const headers = { 'Accept': 'application/vnd.github.v3+json' }; - if (githubToken) headers['Authorization'] = `token ${githubToken}`; - let useMergedStatus = false; - let fallbackToSimple = false; - let daysRange = getDaysBetween(startingDate, endingDate); - if (githubToken) { - useMergedStatus = true; - } else if (daysRange <= 7) { - useMergedStatus = true; - } + if (!items) { + logError('No items to process for writeGithubIssuesPrs'); + return; + } + if (!items.length) { + logError('No items to process for writeGithubIssuesPrs'); + return; + } + const headers = { 'Accept': 'application/vnd.github.v3+json' }; + if (githubToken) headers['Authorization'] = `token ${githubToken}`; + let useMergedStatus = false; + let fallbackToSimple = false; + let daysRange = getDaysBetween(startingDate, endingDate); + if (githubToken) { + useMergedStatus = true; + } else if (daysRange <= 7) { + useMergedStatus = true; + } - let prsToCheck = []; - for (let i = 0; i < items.length; i++) { - let item = items[i]; - if (item.pull_request && item.state === 'closed' && useMergedStatus && !fallbackToSimple) { - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; + let prsToCheck = []; + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (item.pull_request && item.state === 'closed' && useMergedStatus && !fallbackToSimple) { + let repository_url = item.repository_url; + if (!repository_url) { + logError('repository_url is undefined for item:', item); + continue; + } + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + prsToCheck.push({ owner, repo, number: item.number, idx: i }); } - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - prsToCheck.push({ owner, repo, number: item.number, idx: i }); } - } - let mergedStatusResults = {}; - if (githubToken) { - // Use GraphQL batching for all cases - if (prsToCheck.length > 0) { - mergedStatusResults = await fetchPrsMergedStatusBatch(prsToCheck, headers); - } - } else if (useMergedStatus) { - if (prsToCheck.length > 30) { - fallbackToSimple = true; - if (typeof Materialize !== 'undefined' && Materialize.toast) { - Materialize.toast('API limit exceeded. Please use a GitHub token for full status. Showing only open/closed PRs.', 5000); + let mergedStatusResults = {}; + if (githubToken) { + // Use GraphQL batching for all cases + if (prsToCheck.length > 0) { + mergedStatusResults = await fetchPrsMergedStatusBatch(prsToCheck, headers); } - } else { - // Use REST API for each PR, cache results - for (let pr of prsToCheck) { - let merged = await fetchPrMergedStatusREST(pr.owner, pr.repo, pr.number, headers); - mergedStatusResults[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; + } else if (useMergedStatus) { + if (prsToCheck.length > 30) { + fallbackToSimple = true; + if (typeof Materialize !== 'undefined' && Materialize.toast) { + Materialize.toast('API limit exceeded. Please use a GitHub token for full status. Showing only open/closed PRs.', 5000); + } + } else { + // Use REST API for each PR, cache results + for (let pr of prsToCheck) { + let merged = await fetchPrMergedStatusREST(pr.owner, pr.repo, pr.number, headers); + mergedStatusResults[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; + } } } - } - for (let i = 0; i < items.length; i++) { - let item = items[i]; - log('[SCRUM-DEBUG] Processing item:', item); - // For GitLab, treat all items in the MRs array as MRs - let isMR = !!item.pull_request; // works for both GitHub and mapped GitLab data - log('[SCRUM-DEBUG] isMR:', isMR, 'platform:', platform, 'item:', item); - let html_url = item.html_url; - let repository_url = item.repository_url; - if (!repository_url) { - logError('repository_url is undefined for item:', item); - continue; - } - let project = repository_url.substr(repository_url.lastIndexOf('/') + 1); - let title = item.title; - let number = item.number; - let li = ''; - - let isDraft = false; - if (isMR && typeof item.draft !== 'undefined') { - isDraft = item.draft; - } + for (let i = 0; i < items.length; i++) { + let item = items[i]; + log('[SCRUM-DEBUG] Processing item:', item); + // For GitLab, treat all items in the MRs array as MRs + let isMR = !!item.pull_request; // works for both GitHub and mapped GitLab data + log('[SCRUM-DEBUG] isMR:', isMR, 'platform:', platform, 'item:', item); + let html_url = item.html_url; + let repository_url = item.repository_url; + // Use project name for GitLab, repo extraction for GitHub + let project = (platform === 'gitlab' && item.project) ? item.project : (repository_url ? repository_url.substr(repository_url.lastIndexOf('/') + 1) : ''); + let title = item.title; + let number = item.number; + let li = ''; + + let isDraft = false; + if (isMR && typeof item.draft !== 'undefined') { + isDraft = item.draft; + } - if (isMR) { - // Platform-specific label - let prAction = ''; + if (isMR) { + // Platform-specific label + let prAction = ''; - const prCreatedDate = new Date(item.created_at); - const startDate = new Date(startingDate); - const endDate = new Date(endingDate + 'T23:59:59'); - const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; + const prCreatedDate = new Date(item.created_at); + const startDate = new Date(startingDate); + const endDate = new Date(endingDate + 'T23:59:59'); + const isNewPR = prCreatedDate >= startDate && prCreatedDate <= endDate; - if (platform === 'github') { - if (!isNewPR) { - const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; - if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits + if (platform === 'github') { + if (!isNewPR) { + const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; + if (!hasCommitsInRange) { + continue; //skip these prs - created outside daterange with no commits + } } + prAction = isNewPR ? 'Made PR' : 'Existing PR'; + } else if (platform === 'gitlab') { + prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; } - prAction = isNewPR ? 'Made PR' : 'Existing PR'; - } else if (platform === 'gitlab') { - prAction = isNewPR ? 'Made Merge Request' : 'Existing Merge Request'; - } - if (isDraft) { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_draft_button}
  • `; - } else if (item.state === 'open') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_open_button}`; - if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { - log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); - item._allCommits.forEach(commit => { - li += `
  • ${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
  • `; - }); - } - li += ``; - } else if (platform === 'gitlab' && item.state === 'closed') { - // For GitLab, always show closed label for closed MRs - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } else { - // GitHub: check merged status if possible - let merged = null; - if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { - let repoParts = repository_url.split('/'); - let owner = repoParts[repoParts.length - 2]; - let repo = repoParts[repoParts.length - 1]; - merged = mergedStatusResults[`${owner}/${repo}#${number}`]; - } - if (merged === true) { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; + if (isDraft) { + li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_draft_button}
  • `; + } else if (item.state === 'open' || item.state === 'opened') { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_open_button}`; + if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { + log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); + item._allCommits.forEach(commit => { + li += `
  • ${commit.messageHeadline} (${new Date(commit.committedDate).toLocaleString()})
  • `; + }); + } + li += ``; + } else if (platform === 'gitlab' && item.state === 'closed') { + // For GitLab, always show closed label for closed MRs + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; } else { + // GitHub: check merged status if possible + let merged = null; + if ((githubToken || (useMergedStatus && !fallbackToSimple)) && mergedStatusResults) { + let repoParts = repository_url.split('/'); + let owner = repoParts[repoParts.length - 2]; + let repo = repoParts[repoParts.length - 1]; + merged = mergedStatusResults[`${owner}/${repo}#${number}`]; + } + if (merged === true) { + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; + } else { - // Always show closed label for merged === false or merged === null/undefined - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + // Always show closed label for merged === false or merged === null/undefined + li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + } } - } - log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); - lastWeekArray.push(li); - continue; // Prevent issue logic from overwriting PR li - } else { - // Only process as issue if not a PR - if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { - let li2 = - '
  • (' + - project + - ') - Work on Issue(#' + - number + - ") - " + - title + - ' ' + - issue_opened_button + - '  
  • '; - nextWeekArray.push(li2); - } - if (item.state === 'open') { - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; - } else if (item.state === 'closed') { - // Always show closed label for closed issues - li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); + lastWeekArray.push(li); + continue; // Prevent issue logic from overwriting PR li } else { - // Fallback for unexpected state - li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; - } + // Only process as issue if not a PR + if (item.state === 'open' && item.body?.toUpperCase().indexOf('YES') > 0) { + let li2 = + '
  • (' + + project + + ') - Work on Issue(#' + + number + + ") - " + + title + + ' ' + + issue_opened_button + + '  
  • '; + nextWeekArray.push(li2); + } + if (item.state === 'open' || item.state === 'opened') { + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_opened_button}
  • `; + } else if (item.state === 'closed') { + // Always show closed label for closed issues + li = `
  • (${project}) - Opened Issue(#${number}) - ${title} ${issue_closed_button}
  • `; + } else { + // Fallback for unexpected state + li = `
  • (${project}) - Opened Issue(#${number}) - ${title}
  • `; + } - log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item); - lastWeekArray.push(li); + log('[SCRUM-DEBUG] Added issue to lastWeekArray:', li, item); + lastWeekArray.push(li); + } } + log('[SCRUM-DEBUG] Final lastWeekArray:', lastWeekArray); + issuesDataProcessed = true; + } - log('[SCRUM-DEBUG] Final lastWeekArray:', lastWeekArray); - issuesDataProcessed = true; -} + let intervalBody = setInterval(() => { + if (!window.emailClientAdapter) return; -let intervalBody = setInterval(() => { - if (!window.emailClientAdapter) return; + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.body) return; - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.body) return; + clearInterval(intervalBody); + scrumBody = elements.body; + }, 500); - clearInterval(intervalBody); - scrumBody = elements.body; -}, 500); + let intervalSubject = setInterval(() => { + const userData = platform === 'gitlab' ? (githubUserData || platformUsername) : githubUserData; + if (!userData || !window.emailClientAdapter) return; -let intervalSubject = setInterval(() => { - const userData = platform === 'gitlab' ? (githubUserData || platformUsername) : githubUserData; - if (!userData || !window.emailClientAdapter) return; + const elements = window.emailClientAdapter.getEditorElements(); + if (!elements || !elements.subject) return; - const elements = window.emailClientAdapter.getEditorElements(); - if (!elements || !elements.subject) return; + if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { + console.log('Not a new conversation, skipping subject interval'); + clearInterval(intervalSubject); + return; + } - if (outputTarget === 'email' && !window.emailClientAdapter.isNewConversation()) { - console.log('Not a new conversation, skipping subject interval'); clearInterval(intervalSubject); - return; - } - - clearInterval(intervalSubject); - scrumSubject = elements.subject; + scrumSubject = elements.subject; - setTimeout(() => { - scrumSubjectLoaded(); + setTimeout(() => { + scrumSubjectLoaded(); + }, 500); }, 500); -}, 500); -// check for github safe writing -let intervalWriteGithubIssues = setInterval(() => { - if (outputTarget === 'popup') { - return; - } else { - const username = platform === 'gitlab' ? platformUsername : githubUsername; - if (scrumBody && username && githubIssuesData && githubPrsReviewData) { - clearInterval(intervalWriteGithubIssues); - clearInterval(intervalWriteGithubPrs); - writeGithubIssuesPrs(); - } - } -}, 500); -let intervalWriteGithubPrs = setInterval(() => { - if (outputTarget === 'popup') { - return; - } else { - const username = platform === 'gitlab' ? platformUsername : githubUsername; - if (scrumBody && username && githubPrsReviewData && githubIssuesData) { - clearInterval(intervalWriteGithubPrs); - clearInterval(intervalWriteGithubIssues); - writeGithubPrsReviews(); + // check for github safe writing + let intervalWriteGithubIssues = setInterval(() => { + if (outputTarget === 'popup') { + return; + } else { + const username = platform === 'gitlab' ? platformUsername : platformUsernameLocal; + if (scrumBody && username && githubIssuesData && githubPrsReviewData) { + clearInterval(intervalWriteGithubIssues); + clearInterval(intervalWriteGithubPrs); + writeGithubIssuesPrs(); + } } - } - }, 500); - - if (!refreshButton_Placed) { - let intervalWriteButton = setInterval(() => { - if (document.getElementsByClassName('F0XO1GC-x-b').length == 3 && scrumBody && enableToggle) { - refreshButton_Placed = true; - clearInterval(intervalWriteButton); - let td = document.createElement('td'); - let button = document.createElement('button'); - button.style = 'background-image:none;background-color:#3F51B5;'; - button.setAttribute('class', 'F0XO1GC-n-a F0XO1GC-G-a'); - button.title = 'Rewrite your SCRUM using updated settings!'; - button.id = 'refreshButton'; - let elemText = document.createTextNode('↻ Rewrite SCRUM!'); - button.appendChild(elemText); - td.appendChild(button); - document.getElementsByClassName('F0XO1GC-x-b')[0].children[0].children[0].appendChild(td); - document.getElementById('refreshButton').addEventListener('click', handleRefresh); - } - }, 1000); + }, 500); + let intervalWriteGithubPrs = setInterval(() => { + if (outputTarget === 'popup') { + return; + } else { + const username = platform === 'gitlab' ? platformUsername : platformUsernameLocal; + if (scrumBody && username && githubPrsReviewData && githubIssuesData) { + clearInterval(intervalWriteGithubPrs); + clearInterval(intervalWriteGithubIssues); + writeGithubPrsReviews(); + } } + }, 500); - function handleRefresh() { - hasInjectedContent = false; // Reset the flag before refresh - allIncluded(); - } + if (!refreshButton_Placed) { + let intervalWriteButton = setInterval(() => { + if (document.getElementsByClassName('F0XO1GC-x-b').length == 3 && scrumBody && enableToggle) { + refreshButton_Placed = true; + clearInterval(intervalWriteButton); + let td = document.createElement('td'); + let button = document.createElement('button'); + button.style = 'background-image:none;background-color:#3F51B5;'; + button.setAttribute('class', 'F0XO1GC-n-a F0XO1GC-G-a'); + button.title = 'Rewrite your SCRUM using updated settings!'; + button.id = 'refreshButton'; + let elemText = document.createTextNode('↻ Rewrite SCRUM!'); + button.appendChild(elemText); + td.appendChild(button); + document.getElementsByClassName('F0XO1GC-x-b')[0].children[0].children[0].appendChild(td); + document.getElementById('refreshButton').addEventListener('click', handleRefresh); + } + }, 1000); } + function handleRefresh() { + hasInjectedContent = false; // Reset the flag before refresh + allIncluded(); + } +} - async function forceGithubDataRefresh() { - let showCommits = false; - - await new Promise(resolve => { - chrome.storage.local.get('showCommits', (result) => { - if (result.showCommits !== undefined) { - showCommits = result.showCommits; - } - resolve(); - }); - }); - if (typeof githubCache !== 'undefined') { - githubCache.data = null; - githubCache.cacheKey = null; - githubCache.timestamp = 0; - githubCache.subject = null; - githubCache.fetching = false; - githubCache.queue = []; - } +async function forceGithubDataRefresh() { + let showCommits = false; - await new Promise(resolve => { - chrome.storage.local.remove('githubCache', resolve); + await new Promise(resolve => { + chrome.storage.local.get('showCommits', (result) => { + if (result.showCommits !== undefined) { + showCommits = result.showCommits; + } + resolve(); }); + }); - chrome.storage.local.set({ showCommits: showCommits }); + if (typeof githubCache !== 'undefined') { + githubCache.data = null; + githubCache.cacheKey = null; + githubCache.timestamp = 0; + githubCache.subject = null; + githubCache.fetching = false; + githubCache.queue = []; + } - hasInjectedContent = false; + await new Promise(resolve => { + chrome.storage.local.remove('githubCache', resolve); + }); - return { success: true }; - } + chrome.storage.local.set({ showCommits: showCommits }); + hasInjectedContent = false; - if (window.location.protocol.startsWith('http')) { - allIncluded('email'); - $('button>span:contains(New conversation)').parent('button').click(() => { - allIncluded(); - }); - } + return { success: true }; +} - window.generateScrumReport = function () { - allIncluded('popup'); - }; - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'forceRefresh') { - forceGithubDataRefresh() - .then(result => sendResponse(result)).catch(err => { - console.error('Force refresh failed:', err); - sendResponse({ success: false, error: err.message }); - }); - return true; - } +if (window.location.protocol.startsWith('http')) { + allIncluded('email'); + $('button>span:contains(New conversation)').parent('button').click(() => { + allIncluded(); }); +} + +window.generateScrumReport = function () { + allIncluded('popup'); +}; - async function fetchPrsMergedStatusBatch(prs, headers) { - const results = {}; - if (prs.length === 0) return results; - const query = `query { +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'forceRefresh') { + forceGithubDataRefresh() + .then(result => sendResponse(result)).catch(err => { + console.error('Force refresh failed:', err); + sendResponse({ success: false, error: err.message }); + }); + return true; + } +}); + +async function fetchPrsMergedStatusBatch(prs, headers) { + const results = {}; + if (prs.length === 0) return results; + const query = `query { ${prs.map((pr, i) => ` repo${i}: repository(owner: \"${pr.owner}\", name: \"${pr.repo}\") { pr${i}: pullRequest(number: ${pr.number}) { merged } }`).join('\n')} }`; - try { - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query }), - }); - if (!res.ok) return results; - const data = await res.json(); - prs.forEach((pr, i) => { - const merged = data.data[`repo${i}`]?.[`pr${i}`]?.merged; - results[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; - }); - return results; - } catch (e) { - return results; - } - + try { + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + if (!res.ok) return results; + const data = await res.json(); + prs.forEach((pr, i) => { + const merged = data.data[`repo${i}`]?.[`pr${i}`]?.merged; + results[`${pr.owner}/${pr.repo}#${pr.number}`] = merged; + }); + return results; + } catch (e) { + return results; } - let selectedRepos = []; - let useRepoFilter = false; +} - async function fetchUserRepositories(username, token, org = '') { - const headers = { - 'Accept': 'application/vnd.github.v3+json', - }; +let selectedRepos = []; +let useRepoFilter = false; - if (token) { - headers['Authorization'] = `token ${token}`; - } +async function fetchUserRepositories(username, token, org = '') { + const headers = { + 'Accept': 'application/vnd.github.v3+json', + }; - if (!username) { - throw new Error('GitHub username is required'); - } + if (token) { + headers['Authorization'] = `token ${token}`; + } - console.log('Fetching repos for username:', username, 'org:', org); + if (!username) { + throw new Error('GitHub username is required'); + } - try { - let dateRange = ''; - try { - const storageData = await new Promise(resolve => { - chrome.storage.local.get(['startingDate', 'endingDate', 'lastWeekContribution', 'yesterdayContribution'], resolve); - }); + console.log('Fetching repos for username:', username, 'org:', org); - let startDate, endDate; - if (storageData.lastWeekContribution) { - const today = new Date(); - const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - startDate = lastWeek.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; - } else if (storageData.yesterdayContribution) { - const today = new Date(); - const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); - startDate = yesterday.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; - } else if (storageData.startingDate && storageData.endingDate) { - startDate = storageData.startingDate; - endDate = storageData.endingDate; - } else { - const today = new Date(); - const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - startDate = lastWeek.toISOString().split('T')[0]; - endDate = today.toISOString().split('T')[0]; - } + try { + let dateRange = ''; + try { + const storageData = await new Promise(resolve => { + chrome.storage.local.get(['startingDate', 'endingDate', 'lastWeekContribution', 'yesterdayContribution'], resolve); + }); - dateRange = `+created:${startDate}..${endDate}`; - console.log(`Using date range for repo search: ${startDate} to ${endDate}`); - } catch (err) { - console.warn('Could not determine date range, using last 30 days:', err); + let startDate, endDate; + if (storageData.lastWeekContribution) { const today = new Date(); - const thirtyDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 30); - const startDate = thirtyDaysAgo.toISOString().split('T')[0]; - const endDate = today.toISOString().split('T')[0]; + const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + startDate = lastWeek.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; + } else if (storageData.yesterdayContribution) { + const today = new Date(); + const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); + startDate = yesterday.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; + } else if (storageData.startingDate && storageData.endingDate) { + startDate = storageData.startingDate; + endDate = storageData.endingDate; + } else { + const today = new Date(); + const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + startDate = lastWeek.toISOString().split('T')[0]; + endDate = today.toISOString().split('T')[0]; } - let orgPart = org && org !== 'all' ? `+org:${org}` : ''; - const issuesUrl = `https://api.github.com/search/issues?q=author:${username}${orgPart}${dateRange}&per_page=100`; - const commentsUrl = `https://api.github.com/search/issues?q=commenter:${username}${orgPart}${dateRange.replace('created:', 'updated:')}&per_page=100`; - console.log('Search URLs:', { issuesUrl, commentsUrl }); + dateRange = `+created:${startDate}..${endDate}`; + console.log(`Using date range for repo search: ${startDate} to ${endDate}`); + } catch (err) { + console.warn('Could not determine date range, using last 30 days:', err); + const today = new Date(); + const thirtyDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 30); + const startDate = thirtyDaysAgo.toISOString().split('T')[0]; + const endDate = today.toISOString().split('T')[0]; + } + let orgPart = org && org !== 'all' ? `+org:${org}` : ''; + const issuesUrl = `https://api.github.com/search/issues?q=author:${username}${orgPart}${dateRange}&per_page=100`; + const commentsUrl = `https://api.github.com/search/issues?q=commenter:${username}${orgPart}${dateRange.replace('created:', 'updated:')}&per_page=100`; - const [issuesRes, commentsRes] = await Promise.all([ - fetch(issuesUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })), - fetch(commentsUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })) - ]); + console.log('Search URLs:', { issuesUrl, commentsUrl }); - let repoSet = new Set(); + const [issuesRes, commentsRes] = await Promise.all([ + fetch(issuesUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })), + fetch(commentsUrl, { headers }).catch(() => ({ ok: false, json: () => ({ items: [] }) })) + ]); - const processRepoItems = (items) => { - items?.forEach(item => { - if (item.repository_url) { - const urlParts = item.repository_url.split('/'); - const repoFullName = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`; - const repoName = `${urlParts[urlParts.length - 1]}` - repoSet.add(repoFullName); - } - }) - } + let repoSet = new Set(); - if (issuesRes.ok) { - const issuesData = await issuesRes.json(); - processRepoItems(issuesData.items); - console.log(`Found ${issuesData.items?.length || 0} issues/PRs authored by user in date range`); - } + const processRepoItems = (items) => { + items?.forEach(item => { + if (item.repository_url) { + const urlParts = item.repository_url.split('/'); + const repoFullName = `${urlParts[urlParts.length - 2]}/${urlParts[urlParts.length - 1]}`; + const repoName = `${urlParts[urlParts.length - 1]}` + repoSet.add(repoFullName); + } + }) + } - if (commentsRes.ok) { - const commentsData = await commentsRes.json(); - processRepoItems(commentsData.items); - console.log(`Found ${commentsData.items?.length || 0} issues/PRs with user comments in date range`); - } + if (issuesRes.ok) { + const issuesData = await issuesRes.json(); + processRepoItems(issuesData.items); + console.log(`Found ${issuesData.items?.length || 0} issues/PRs authored by user in date range`); + } - const repoNames = Array.from(repoSet); - console.log(`Found ${repoNames.length} unique repositories with contributions in the selected date range`); + if (commentsRes.ok) { + const commentsData = await commentsRes.json(); + processRepoItems(commentsData.items); + console.log(`Found ${commentsData.items?.length || 0} issues/PRs with user comments in date range`); + } - if (repoNames.length === 0) { - console.log(`No repositories with contrbutions found in the selected date range`); - return []; - } + const repoNames = Array.from(repoSet); + console.log(`Found ${repoNames.length} unique repositories with contributions in the selected date range`); + + if (repoNames.length === 0) { + console.log(`No repositories with contrbutions found in the selected date range`); + return []; + } - const repoFields = ` + const repoFields = ` name nameWithOwner description @@ -1568,90 +1520,90 @@ ${prs.map((pr, i) => ` repo${i}: repository(owner: \"${pr.owner}\", name: \"${pr } `; - const repoQueries = repoNames.slice(0, 50).map((repoFullName, i) => { - const parts = repoFullName.split('/'); - if (parts.length !== 2) return ''; - const owner = parts[0]; - const repo = parts[1]; - return ` + const repoQueries = repoNames.slice(0, 50).map((repoFullName, i) => { + const parts = repoFullName.split('/'); + if (parts.length !== 2) return ''; + const owner = parts[0]; + const repo = parts[1]; + return ` repo${i}: repository(owner: "${owner}", name: "${repo}") { ... on Repository { ${repoFields} } } `; - }).join('\n'); + }).join('\n'); - const query = `query { ${repoQueries} }`; + const query = `query { ${repoQueries} }`; - try { - const res = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query }), - }); + try { + const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); - if (!res.ok) { - throw new Error(`GraphQL request for repos failed: ${res.status}`); - } + if (!res.ok) { + throw new Error(`GraphQL request for repos failed: ${res.status}`); + } - const graphQLData = await res.json(); + const graphQLData = await res.json(); - if (graphQLData.errors) { - logError("GraphQL errors fetching repos:", graphQLData.errors); - return []; - } + if (graphQLData.errors) { + logError("GraphQL errors fetching repos:", graphQLData.errors); + return []; + } - const repos = Object.values(graphQLData.data) - .filter(repo => repo !== null) - .map(repo => ({ - name: repo.name, - fullName: repo.nameWithOwner, - description: repo.description, - language: repo.primaryLanguage ? repo.primaryLanguage.name : null, - updatedAt: repo.pushedAt, - stars: repo.stargazerCount - })); + const repos = Object.values(graphQLData.data) + .filter(repo => repo !== null) + .map(repo => ({ + name: repo.name, + fullName: repo.nameWithOwner, + description: repo.description, + language: repo.primaryLanguage ? repo.primaryLanguage.name : null, + updatedAt: repo.pushedAt, + stars: repo.stargazerCount + })); - console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); - return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); + console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); + return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); - } catch (err) { - logError('Failed to fetch repository details via GraphQL:', err); - throw err; - } } catch (err) { - logError('Failed to fetch repositories:', err); + logError('Failed to fetch repository details via GraphQL:', err); throw err; } + } catch (err) { + logError('Failed to fetch repositories:', err); + throw err; } +} - function filterDataByRepos(data, selectedRepos) { - if (!selectedRepos || selectedRepos.length === 0) { - return data; - } - - const filteredData = { - ...data, - githubIssuesData: { - ...data.githubIssuesData, - items: data.githubIssuesData?.items?.filter(item => { - const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); - return selectedRepos.includes(repoName); - }) || [] - }, - githubPrsReviewData: { - ...data.githubPrsReviewData, - items: data.githubPrsReviewData?.items?.filter(item => { - const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); - return selectedRepos.includes(repoName); - }) || [] - } - }; - return filteredData; +function filterDataByRepos(data, selectedRepos) { + if (!selectedRepos || selectedRepos.length === 0) { + return data; } - window.fetchUserRepositories = fetchUserRepositories; + + const filteredData = { + ...data, + githubIssuesData: { + ...data.githubIssuesData, + items: data.githubIssuesData?.items?.filter(item => { + const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); + return selectedRepos.includes(repoName); + }) || [] + }, + githubPrsReviewData: { + ...data.githubPrsReviewData, + items: data.githubPrsReviewData?.items?.filter(item => { + const repoName = item.repository_url?.substr(item.repository_url.lastIndexOf('/') + 1); + return selectedRepos.includes(repoName); + }) || [] + } + }; + return filteredData; +} +window.fetchUserRepositories = fetchUserRepositories; From b92ae5a4a7bc2401cf471ed3a373fe7dc52a909e Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 01:42:51 +0530 Subject: [PATCH 24/40] fixed bug --- src/scripts/scrumHelper.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 241aeee..681ec2d 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -118,6 +118,10 @@ function allIncluded(outputTarget = 'email') { if (typeof items.enableToggle !== 'undefined') { enableToggle = items.enableToggle; } + // Fix: Assign missing variables from storage + showCommits = items.showCommits || false; + orgName = items.orgName || ''; + if (items.lastWeekContribution) { handleLastWeekContributionChange(); } else if (items.yesterdayContribution) { @@ -547,13 +551,13 @@ function allIncluded(outputTarget = 'email') { if (githubIssuesData && githubIssuesData.items) { log('Fetched githubIssuesData:', githubIssuesData.items.length, 'items'); - // Collect open PRs + // Collect only open PRs for commit fetching const openPRs = githubIssuesData.items.filter( item => item.pull_request && item.state === 'open' ); log('Open PRs for commit fetching:', openPRs.map(pr => pr.number)); - // Fetch commits for open PRs (batch) - if (openPRs.length && githubToken) { + // Fetch commits for open PRs (batch) if showCommits is enabled + if (openPRs.length && githubToken && showCommits) { const commitMap = await fetchCommitsForOpenPRs(openPRs, githubToken, startingDate, endingDate); log('Commit map returned from fetchCommitsForOpenPRs:', commitMap); // Attach commits to PR objects @@ -1150,9 +1154,13 @@ ${userReason}`; if (platform === 'github') { if (!isNewPR) { + // Only show existing PRs if they are open and have commits in the date range + if (item.state !== 'open') { + continue; // Skip closed/merged existing PRs + } const hasCommitsInRange = showCommits && item._allCommits && item._allCommits.length > 0; if (!hasCommitsInRange) { - continue; //skip these prs - created outside daterange with no commits + continue; // Skip existing PRs without commits in date range } } prAction = isNewPR ? 'Made PR' : 'Existing PR'; @@ -1188,10 +1196,8 @@ ${userReason}`; if (merged === true) { li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_merged_button}
  • `; } else { - // Always show closed label for merged === false or merged === null/undefined li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; - } } log('[SCRUM-DEBUG] Added PR/MR to lastWeekArray:', li, item); From 44f21556d3d94b85d7c7b3ed9576d9db2ee5aedb Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 10:51:40 +0530 Subject: [PATCH 25/40] syncronize injection --- src/scripts/scrumHelper.js | 190 +++++++++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 52 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 681ec2d..9a3bc85 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -121,7 +121,7 @@ function allIncluded(outputTarget = 'email') { // Fix: Assign missing variables from storage showCommits = items.showCommits || false; orgName = items.orgName || ''; - + if (items.lastWeekContribution) { handleLastWeekContributionChange(); } else if (items.yesterdayContribution) { @@ -171,50 +171,111 @@ function allIncluded(outputTarget = 'email') { generateBtn.innerHTML = ' Generating...'; generateBtn.disabled = true; } - gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate) - .then(data => { - console.log('[SCRUM-HELPER] GitLab data:', data); - // Map GitLab issues and MRs to the expected structure for both popup and email - function mapGitLabItem(item, projects, type) { - const project = projects.find(p => p.id === item.project_id); - const repoName = project ? project.name : 'unknown'; // Use project.name for display - return { - ...item, - repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, - html_url: type === 'issue' - ? (item.web_url || (project ? `${project.web_url}/-/issues/${item.iid}` : '')) - : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), - number: item.iid, - title: item.title, - state: item.state, - project: repoName, - pull_request: type === 'mr', + // --- FIX START: Always await GitLab data before injecting for email --- + if (outputTarget === 'email') { + (async () => { + try { + const data = await gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate); + // Map GitLab issues and MRs to the expected structure for both popup and email + function mapGitLabItem(item, projects, type) { + const project = projects.find(p => p.id === item.project_id); + const repoName = project ? project.name : 'unknown'; // Use project.name for display + return { + ...item, + repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, + html_url: type === 'issue' + ? (item.web_url || (project ? `${project.web_url}/-/issues/${item.iid}` : '')) + : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), + number: item.iid, + title: item.title, + state: item.state, + project: repoName, + pull_request: type === 'mr', + }; + } + const mappedIssues = (data.issues || []).map(issue => mapGitLabItem(issue, data.projects, 'issue')); + const mappedMRs = (data.mergeRequests || data.mrs || []).map(mr => mapGitLabItem(mr, data.projects, 'mr')); + const mappedData = { + githubIssuesData: { items: mappedIssues }, + githubPrsReviewData: { items: mappedMRs }, + githubUserData: data.user || {}, }; + githubUserData = mappedData.githubUserData; + // Generate subject using real name if available + let name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; + let project = projectName || ''; + let curDate = new Date(); + let year = curDate.getFullYear().toString(); + let date = curDate.getDate(); + let month = curDate.getMonth() + 1; + if (month < 10) month = '0' + month; + if (date < 10) date = '0' + date; + let dateCode = year.toString() + month.toString() + date.toString(); + const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; + // Process data to generate the scrum body (but do not inject yet) + await processGithubData(mappedData, true, subject); + scrumGenerationInProgress = false; + } catch (err) { + console.error('GitLab fetch failed:', err); + if (outputTarget === 'popup') { + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; + } + } + scrumGenerationInProgress = false; } - const mappedIssues = (data.issues || []).map(issue => mapGitLabItem(issue, data.projects, 'issue')); - const mappedMRs = (data.mergeRequests || data.mrs || []).map(mr => mapGitLabItem(mr, data.projects, 'mr')); - const mappedData = { - githubIssuesData: { items: mappedIssues }, - githubPrsReviewData: { items: mappedMRs }, - githubUserData: data.user || {}, - }; - processGithubData(mappedData); - scrumGenerationInProgress = false; - }) - .catch(err => { - console.error('GitLab fetch failed:', err); - if (outputTarget === 'popup') { - if (generateBtn) { - generateBtn.innerHTML = ' Generate Report'; - generateBtn.disabled = false; + })(); + } else { + // Original flow for popup + gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate) + .then(data => { + function mapGitLabItem(item, projects, type) { + const project = projects.find(p => p.id === item.project_id); + const repoName = project ? project.name : 'unknown'; + return { + ...item, + repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, + html_url: type === 'issue' + ? (item.web_url || (project ? `${project.web_url}/-/issues/${item.iid}` : '')) + : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), + number: item.iid, + title: item.title, + state: item.state, + project: repoName, + pull_request: type === 'mr', + }; } - const scrumReport = document.getElementById('scrumReport'); - if (scrumReport) { - scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; + const mappedIssues = (data.issues || []).map(issue => mapGitLabItem(issue, data.projects, 'issue')); + const mappedMRs = (data.mergeRequests || data.mrs || []).map(mr => mapGitLabItem(mr, data.projects, 'mr')); + const mappedData = { + githubIssuesData: { items: mappedIssues }, + githubPrsReviewData: { items: mappedMRs }, + githubUserData: data.user || {}, + }; + processGithubData(mappedData); + scrumGenerationInProgress = false; + }) + .catch(err => { + console.error('GitLab fetch failed:', err); + if (outputTarget === 'popup') { + if (generateBtn) { + generateBtn.innerHTML = ' Generate Report'; + generateBtn.disabled = false; + } + const scrumReport = document.getElementById('scrumReport'); + if (scrumReport) { + scrumReport.innerHTML = `
    ${err.message || 'An error occurred while fetching GitLab data.'}
    `; + } } - } - scrumGenerationInProgress = false; - }); + scrumGenerationInProgress = false; + }); + } + // --- FIX END --- } else { if (outputTarget === 'popup') { const scrumReport = document.getElementById('scrumReport'); @@ -766,8 +827,7 @@ function allIncluded(outputTarget = 'email') { } } - async function processGithubData(data) { - + async function processGithubData(data, injectEmailTogether = false, subjectForEmail = null) { githubIssuesData = data.githubIssuesData; githubPrsReviewData = data.githubPrsReviewData; githubUserData = data.githubUserData; @@ -777,33 +837,59 @@ function allIncluded(outputTarget = 'email') { user: githubUserData?.login, filtered: useRepoFilter }); - lastWeekArray = []; nextWeekArray = []; reviewedPrsArray = []; githubPrsReviewDataProcessed = {}; issuesDataProcessed = false; prsReviewDataProcessed = false; - - // Update subject - if (!githubCache.subject && scrumSubject) { scrumSubjectLoaded(); } log('[SCRUM-DEBUG] Processing issues for main activity:', githubIssuesData?.items); - if (platform === 'github') { await writeGithubIssuesPrs(githubIssuesData?.items || []); } else if (platform === 'gitlab') { await writeGithubIssuesPrs(githubIssuesData?.items || []); await writeGithubIssuesPrs(githubPrsReviewData?.items || []); } - - - await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); - writeScrumBody(); + if (injectEmailTogether && subjectForEmail) { + // Synchronized subject and body injection for GitLab email + let lastWeekUl = '
      '; + for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; + for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; + lastWeekUl += '
    '; + let nextWeekUl = '
      '; + for (let i = 0; i < nextWeekArray.length; i++) nextWeekUl += nextWeekArray[i]; + nextWeekUl += '
    '; + let weekOrDay = lastWeekContribution ? 'last week' : (yesterdayContribution ? 'yesterday' : 'the period'); + let weekOrDay2 = lastWeekContribution ? 'this week' : 'today'; + let content; + if (lastWeekContribution == true || yesterdayContribution == true) { + content = `1. What did I do ${weekOrDay}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; + } else { + content = `1. What did I do from ${formatDate(startingDate)} to ${formatDate(endingDate)}?
    ${lastWeekUl}
    2. What do I plan to do ${weekOrDay2}?
    ${nextWeekUl}
    3. What is blocking me from making progress?
    ${userReason}`; + } + // Wait for both subject and body to be available, then inject both + let injected = false; + let interval = setInterval(() => { + const elements = window.emailClientAdapter?.getEditorElements(); + if (elements && elements.subject && elements.body && !injected) { + elements.subject.value = subjectForEmail; + elements.subject.dispatchEvent(new Event('input', { bubbles: true })); + window.emailClientAdapter.injectContent(elements.body, content, elements.eventTypes.contentChange); + injected = true; + clearInterval(interval); + } + }, 200); + setTimeout(() => { + if (!injected) clearInterval(interval); + }, 30000); + } else { + writeScrumBody(); + } } function formatDate(dateString) { From 6092316d0ef138d5e6477a881036474c1c47b460 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 11:17:42 +0530 Subject: [PATCH 26/40] removed testing logs --- src/scripts/popup.js | 74 +++++++++++++++++++++++++++++--------- src/scripts/scrumHelper.js | 51 ++++++++++++-------------- 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 25b0a9f..4463702 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -484,15 +484,7 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.set({ platformUsername: platformUsername.value }); }); - // Platform change: clear username, update label/placeholder, clear storage - // This is handled by the new dropdown system, so removing this old listener - // platformSelect.addEventListener('change', function () { - // const platform = platformSelect.value; - // chrome.storage.local.set({ platform }); - // updatePlatformUI(platform); - // platformUsername.value = ''; - // chrome.storage.local.set({ platformUsername: '' }); - // }); + } function showReportView() { @@ -500,7 +492,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.remove('hidden'); settingsSection.classList.add('hidden'); settingsToggle.classList.remove('active'); - console.log('Switched to report view'); + } function showSettingsView() { @@ -508,7 +500,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.add('hidden'); settingsSection.classList.remove('hidden'); settingsToggle.classList.add('active'); - console.log('Switched to settings view'); + } if (settingsToggle) { @@ -574,6 +566,19 @@ document.addEventListener('DOMContentLoaded', function () { let highlightedIndex = -1; async function triggerRepoFetchIfEnabled() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + // Do not run repo fetch for non-GitHub platforms + if (repoStatus) repoStatus.textContent = 'Repository filtering is only available for GitHub.'; + return; + } if (!useRepoFilter.checked) { return; } @@ -628,7 +633,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - console.error('Auto refetch failed:', err); + if (repoStatus) { repoStatus.textContent = `Error: ${err.message || 'Failed to refetch repos'}`; } @@ -649,6 +654,20 @@ document.addEventListener('DOMContentLoaded', function () { }); useRepoFilter.addEventListener('change', debounce(async () => { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + repoFilterContainer.classList.add('hidden'); + useRepoFilter.checked = false; + if (repoStatus) repoStatus.textContent = 'Repository filtering is only available for GitHub.'; + return; + } const enabled = useRepoFilter.checked; const hasToken = githubTokenInput.value.trim() !== ''; repoFilterContainer.classList.toggle('hidden', !enabled); @@ -730,7 +749,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - console.error('Auto load repos failed', err); + if (err.message?.includes('401')) { repoStatus.textContent = 'Github token required for private repos'; } else if (err.message?.includes('username')) { @@ -807,6 +826,18 @@ document.addEventListener('DOMContentLoaded', function () { } debugRepoFetch(); async function loadRepos() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + if (repoStatus) repoStatus.textContent = 'Repository loading is only available for GitHub.'; + return; + } console.log('window.fetchUserRepositories exists:', !!window.fetchUserRepositories); console.log('Available functions:', Object.keys(window).filter(key => key.includes('fetch'))); @@ -832,6 +863,18 @@ document.addEventListener('DOMContentLoaded', function () { } async function performRepoFetch() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + if (repoStatus) repoStatus.textContent = 'Repository fetching is only available for GitHub.'; + return; + } console.log('[POPUP-DEBUG] performRepoFetch called.'); repoStatus.textContent = 'Loading repositories...'; repoSearch.classList.add('repository-search-loading'); @@ -1038,10 +1081,7 @@ orgInput.addEventListener('input', function () { // Add click event for setOrgBtn to set org setOrgBtn.addEventListener('click', function () { let org = orgInput.value.trim().toLowerCase(); - // Do not default to any org, allow empty string - // if (!org) { - // org = 'fossasia'; - // } + console.log('[Org Check] Checking organization:', org); if (!org) { // If org is empty, clear orgName in storage but don't auto-generate report diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 9a3bc85..4142be8 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -5,12 +5,8 @@ function log(...args) { console.log(`[SCRUM-HELPER]:`, ...args); } } -function logError(...args) { - if (DEBUG) { - console.error('[SCRUM-HELPER]:', ...args); - } -} -console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); + + let refreshButton_Placed = false; let enableToggle = true; let hasInjectedContent = false; @@ -23,12 +19,12 @@ let gitlabHelper = null; function allIncluded(outputTarget = 'email') { if (scrumGenerationInProgress) { - console.warn('[SCRUM-HELPER]: Scrum generation already in progress, aborting new call.'); + return; } scrumGenerationInProgress = true; console.log('allIncluded called with outputTarget:', outputTarget); - console.log('Current window context:', window.location.href); + let scrumBody = null; let scrumSubject = null; let startingDate = ''; @@ -118,7 +114,7 @@ function allIncluded(outputTarget = 'email') { if (typeof items.enableToggle !== 'undefined') { enableToggle = items.enableToggle; } - // Fix: Assign missing variables from storage + showCommits = items.showCommits || false; orgName = items.orgName || ''; @@ -131,7 +127,7 @@ function allIncluded(outputTarget = 'email') { endingDate = items.endingDate; } else { - handleLastWeekContributionChange(); //when no date is stored i.e on fresh unpack - default to last week. + handleLastWeekContributionChange(); if (outputTarget === 'popup') { chrome.storage.local.set({ lastWeekContribution: true, yesterdayContribution: false }); @@ -139,10 +135,10 @@ function allIncluded(outputTarget = 'email') { } - // PLATFORM LOGIC FIX + if (platform === 'github') { if (platformUsernameLocal) { - console.log("[DEBUG] About to fetch GitHub data for:", platformUsernameLocal); + fetchGithubData(); } else { if (outputTarget === 'popup') { @@ -171,15 +167,15 @@ function allIncluded(outputTarget = 'email') { generateBtn.innerHTML = ' Generating...'; generateBtn.disabled = true; } - // --- FIX START: Always await GitLab data before injecting for email --- + if (outputTarget === 'email') { (async () => { try { const data = await gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate); - // Map GitLab issues and MRs to the expected structure for both popup and email + function mapGitLabItem(item, projects, type) { const project = projects.find(p => p.id === item.project_id); - const repoName = project ? project.name : 'unknown'; // Use project.name for display + const repoName = project ? project.name : 'unknown'; return { ...item, repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, @@ -201,7 +197,7 @@ function allIncluded(outputTarget = 'email') { githubUserData: data.user || {}, }; githubUserData = mappedData.githubUserData; - // Generate subject using real name if available + let name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; let project = projectName || ''; let curDate = new Date(); @@ -212,7 +208,7 @@ function allIncluded(outputTarget = 'email') { if (date < 10) date = '0' + date; let dateCode = year.toString() + month.toString() + date.toString(); const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; - // Process data to generate the scrum body (but do not inject yet) + await processGithubData(mappedData, true, subject); scrumGenerationInProgress = false; } catch (err) { @@ -231,7 +227,7 @@ function allIncluded(outputTarget = 'email') { } })(); } else { - // Original flow for popup + gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate) .then(data => { function mapGitLabItem(item, projects, type) { @@ -1115,13 +1111,10 @@ ${userReason}`; function triggerScrumGeneration() { if (issuesDataProcessed && prsReviewDataProcessed) { - log('Both data sets processed, generating scrum body.'); + writeScrumBody(); } else { - log('Waiting for all data to be processed before generating scrum.', { - issues: issuesDataProcessed, - reviews: prsReviewDataProcessed, - }); + } } @@ -1152,14 +1145,14 @@ ${userReason}`; } async function writeGithubIssuesPrs(items) { - log('writeGithubIssuesPrs called'); + if (!items) { - logError('No items to process for writeGithubIssuesPrs'); + return; } if (!items.length) { - logError('No items to process for writeGithubIssuesPrs'); + return; } const headers = { 'Accept': 'application/vnd.github.v3+json' }; @@ -1660,15 +1653,15 @@ async function fetchUserRepositories(username, token, org = '') { stars: repo.stargazerCount })); - console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); + return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); } catch (err) { - logError('Failed to fetch repository details via GraphQL:', err); + throw err; } } catch (err) { - logError('Failed to fetch repositories:', err); + throw err; } } From 44f8e9a182f20e62c4cdb19c04efd30b5ea99c13 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 11:53:39 +0530 Subject: [PATCH 27/40] removed testing logs --- src/scripts/popup.js | 74 +++++++++++++++++++++++++++++--------- src/scripts/scrumHelper.js | 47 ++++++++++++------------ 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 25b0a9f..4463702 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -484,15 +484,7 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.set({ platformUsername: platformUsername.value }); }); - // Platform change: clear username, update label/placeholder, clear storage - // This is handled by the new dropdown system, so removing this old listener - // platformSelect.addEventListener('change', function () { - // const platform = platformSelect.value; - // chrome.storage.local.set({ platform }); - // updatePlatformUI(platform); - // platformUsername.value = ''; - // chrome.storage.local.set({ platformUsername: '' }); - // }); + } function showReportView() { @@ -500,7 +492,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.remove('hidden'); settingsSection.classList.add('hidden'); settingsToggle.classList.remove('active'); - console.log('Switched to report view'); + } function showSettingsView() { @@ -508,7 +500,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.add('hidden'); settingsSection.classList.remove('hidden'); settingsToggle.classList.add('active'); - console.log('Switched to settings view'); + } if (settingsToggle) { @@ -574,6 +566,19 @@ document.addEventListener('DOMContentLoaded', function () { let highlightedIndex = -1; async function triggerRepoFetchIfEnabled() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + // Do not run repo fetch for non-GitHub platforms + if (repoStatus) repoStatus.textContent = 'Repository filtering is only available for GitHub.'; + return; + } if (!useRepoFilter.checked) { return; } @@ -628,7 +633,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - console.error('Auto refetch failed:', err); + if (repoStatus) { repoStatus.textContent = `Error: ${err.message || 'Failed to refetch repos'}`; } @@ -649,6 +654,20 @@ document.addEventListener('DOMContentLoaded', function () { }); useRepoFilter.addEventListener('change', debounce(async () => { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + repoFilterContainer.classList.add('hidden'); + useRepoFilter.checked = false; + if (repoStatus) repoStatus.textContent = 'Repository filtering is only available for GitHub.'; + return; + } const enabled = useRepoFilter.checked; const hasToken = githubTokenInput.value.trim() !== ''; repoFilterContainer.classList.toggle('hidden', !enabled); @@ -730,7 +749,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - console.error('Auto load repos failed', err); + if (err.message?.includes('401')) { repoStatus.textContent = 'Github token required for private repos'; } else if (err.message?.includes('username')) { @@ -807,6 +826,18 @@ document.addEventListener('DOMContentLoaded', function () { } debugRepoFetch(); async function loadRepos() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + if (repoStatus) repoStatus.textContent = 'Repository loading is only available for GitHub.'; + return; + } console.log('window.fetchUserRepositories exists:', !!window.fetchUserRepositories); console.log('Available functions:', Object.keys(window).filter(key => key.includes('fetch'))); @@ -832,6 +863,18 @@ document.addEventListener('DOMContentLoaded', function () { } async function performRepoFetch() { + // --- PLATFORM CHECK: Only run for GitHub --- + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + if (platform !== 'github') { + if (repoStatus) repoStatus.textContent = 'Repository fetching is only available for GitHub.'; + return; + } console.log('[POPUP-DEBUG] performRepoFetch called.'); repoStatus.textContent = 'Loading repositories...'; repoSearch.classList.add('repository-search-loading'); @@ -1038,10 +1081,7 @@ orgInput.addEventListener('input', function () { // Add click event for setOrgBtn to set org setOrgBtn.addEventListener('click', function () { let org = orgInput.value.trim().toLowerCase(); - // Do not default to any org, allow empty string - // if (!org) { - // org = 'fossasia'; - // } + console.log('[Org Check] Checking organization:', org); if (!org) { // If org is empty, clear orgName in storage but don't auto-generate report diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 9a3bc85..3b87c00 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -5,12 +5,14 @@ function log(...args) { console.log(`[SCRUM-HELPER]:`, ...args); } } + function logError(...args) { if (DEBUG) { console.error('[SCRUM-HELPER]:', ...args); } } -console.log('Script loaded, adapter exists:', !!window.emailClientAdapter); + + let refreshButton_Placed = false; let enableToggle = true; let hasInjectedContent = false; @@ -23,12 +25,12 @@ let gitlabHelper = null; function allIncluded(outputTarget = 'email') { if (scrumGenerationInProgress) { - console.warn('[SCRUM-HELPER]: Scrum generation already in progress, aborting new call.'); + return; } scrumGenerationInProgress = true; console.log('allIncluded called with outputTarget:', outputTarget); - console.log('Current window context:', window.location.href); + let scrumBody = null; let scrumSubject = null; let startingDate = ''; @@ -118,7 +120,7 @@ function allIncluded(outputTarget = 'email') { if (typeof items.enableToggle !== 'undefined') { enableToggle = items.enableToggle; } - // Fix: Assign missing variables from storage + showCommits = items.showCommits || false; orgName = items.orgName || ''; @@ -131,7 +133,7 @@ function allIncluded(outputTarget = 'email') { endingDate = items.endingDate; } else { - handleLastWeekContributionChange(); //when no date is stored i.e on fresh unpack - default to last week. + handleLastWeekContributionChange(); if (outputTarget === 'popup') { chrome.storage.local.set({ lastWeekContribution: true, yesterdayContribution: false }); @@ -139,10 +141,10 @@ function allIncluded(outputTarget = 'email') { } - // PLATFORM LOGIC FIX + if (platform === 'github') { if (platformUsernameLocal) { - console.log("[DEBUG] About to fetch GitHub data for:", platformUsernameLocal); + fetchGithubData(); } else { if (outputTarget === 'popup') { @@ -171,15 +173,15 @@ function allIncluded(outputTarget = 'email') { generateBtn.innerHTML = ' Generating...'; generateBtn.disabled = true; } - // --- FIX START: Always await GitLab data before injecting for email --- + if (outputTarget === 'email') { (async () => { try { const data = await gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate); - // Map GitLab issues and MRs to the expected structure for both popup and email + function mapGitLabItem(item, projects, type) { const project = projects.find(p => p.id === item.project_id); - const repoName = project ? project.name : 'unknown'; // Use project.name for display + const repoName = project ? project.name : 'unknown'; return { ...item, repository_url: `https://gitlab.com/api/v4/projects/${item.project_id}`, @@ -201,7 +203,7 @@ function allIncluded(outputTarget = 'email') { githubUserData: data.user || {}, }; githubUserData = mappedData.githubUserData; - // Generate subject using real name if available + let name = githubUserData?.name || githubUserData?.username || platformUsernameLocal || platformUsername; let project = projectName || ''; let curDate = new Date(); @@ -212,7 +214,7 @@ function allIncluded(outputTarget = 'email') { if (date < 10) date = '0' + date; let dateCode = year.toString() + month.toString() + date.toString(); const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; - // Process data to generate the scrum body (but do not inject yet) + await processGithubData(mappedData, true, subject); scrumGenerationInProgress = false; } catch (err) { @@ -231,7 +233,7 @@ function allIncluded(outputTarget = 'email') { } })(); } else { - // Original flow for popup + gitlabHelper.fetchGitLabData(platformUsernameLocal, startingDate, endingDate) .then(data => { function mapGitLabItem(item, projects, type) { @@ -1115,13 +1117,10 @@ ${userReason}`; function triggerScrumGeneration() { if (issuesDataProcessed && prsReviewDataProcessed) { - log('Both data sets processed, generating scrum body.'); + writeScrumBody(); } else { - log('Waiting for all data to be processed before generating scrum.', { - issues: issuesDataProcessed, - reviews: prsReviewDataProcessed, - }); + } } @@ -1152,14 +1151,14 @@ ${userReason}`; } async function writeGithubIssuesPrs(items) { - log('writeGithubIssuesPrs called'); + if (!items) { - logError('No items to process for writeGithubIssuesPrs'); + return; } if (!items.length) { - logError('No items to process for writeGithubIssuesPrs'); + return; } const headers = { 'Accept': 'application/vnd.github.v3+json' }; @@ -1660,15 +1659,15 @@ async function fetchUserRepositories(username, token, org = '') { stars: repo.stargazerCount })); - console.log(`Successfully fetched details for ${repos.length} repositories via GraphQL`); + return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); } catch (err) { - logError('Failed to fetch repository details via GraphQL:', err); + throw err; } } catch (err) { - logError('Failed to fetch repositories:', err); + throw err; } } From 3710b4021f57cd358b5394c53e290d617091b5fc Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 12:15:28 +0530 Subject: [PATCH 28/40] changes dark mode --- src/index.css | 24 ++++++++++++++++++++++++ src/popup.html | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index 2c65ed2..534feb5 100644 --- a/src/index.css +++ b/src/index.css @@ -688,4 +688,28 @@ hr, color: #2563eb; font-weight: 500; text-decoration: underline; +} + +.dark-mode #platformDropdownBtn { + background-color: #404040 !important; + border-color: #505050 !important; + color: #ffffff !important; +} + +.dark-mode #platformDropdownBtn:focus { + outline: 2px solid #2563eb; +} + +.dark-mode #platformDropdownList { + background-color: #2d2d2d !important; + border-color: #404040 !important; + color: #ffffff !important; +} + +.dark-mode #platformDropdownList li { + color: #ffffff !important; +} + +.dark-mode #platformDropdownList li:hover { + background-color: #374151 !important; } \ No newline at end of file diff --git a/src/popup.html b/src/popup.html index bbd8011..cb1b090 100644 --- a/src/popup.html +++ b/src/popup.html @@ -179,7 +179,7 @@

    Your Username

    - Github Token required, add it in the settings. + Github Token required, add it in the settings(for github platform)
    From 43bf00a4182a1f3f9adac7611ab9a4a1875fe33a Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 14 Jul 2025 12:23:41 +0530 Subject: [PATCH 29/40] z index changed --- src/index.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/index.css b/src/index.css index 534feb5..0555c50 100644 --- a/src/index.css +++ b/src/index.css @@ -700,10 +700,16 @@ hr, outline: 2px solid #2563eb; } +#platformDropdownList { + min-width: unset; + width: unset !important; + z-index: 1000 !important; +} + .dark-mode #platformDropdownList { - background-color: #2d2d2d !important; - border-color: #404040 !important; - color: #ffffff !important; + min-width: unset; + width: unset !important; + z-index: 1000 !important; } .dark-mode #platformDropdownList li { From 20496f8698ee1e1b111772c4386b440d02c69999 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 16 Jul 2025 16:28:55 +0530 Subject: [PATCH 30/40] added hidden class --- src/popup.html | 21 ++++++++------------- src/scripts/popup.js | 30 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/popup.html b/src/popup.html index cb1b090..9fab019 100644 --- a/src/popup.html +++ b/src/popup.html @@ -68,8 +68,7 @@

    Scrum

    @@ -149,13 +148,11 @@

    Your Username

    - +
    - +
    @@ -169,13 +166,12 @@

    Your Username

    -
    +
    -
    -
    +
    No repositories selected (all will be included) @@ -322,7 +317,7 @@
    Scrum Report
    - +

    Enter cache TTL (in minutes) diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 4463702..434fd4c 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -484,7 +484,7 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.set({ platformUsername: platformUsername.value }); }); - + } function showReportView() { @@ -492,7 +492,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.remove('hidden'); settingsSection.classList.add('hidden'); settingsToggle.classList.remove('active'); - + } function showSettingsView() { @@ -500,7 +500,7 @@ document.addEventListener('DOMContentLoaded', function () { reportSection.classList.add('hidden'); settingsSection.classList.remove('hidden'); settingsToggle.classList.add('active'); - + } if (settingsToggle) { @@ -633,7 +633,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - + if (repoStatus) { repoStatus.textContent = `Error: ${err.message || 'Failed to refetch repos'}`; } @@ -749,7 +749,7 @@ document.addEventListener('DOMContentLoaded', function () { } } } catch (err) { - + if (err.message?.includes('401')) { repoStatus.textContent = 'Github token required for private repos'; } else if (err.message?.includes('username')) { @@ -1081,7 +1081,7 @@ orgInput.addEventListener('input', function () { // Add click event for setOrgBtn to set org setOrgBtn.addEventListener('click', function () { let org = orgInput.value.trim().toLowerCase(); - + console.log('[Org Check] Checking organization:', org); if (!org) { // If org is empty, clear orgName in storage but don't auto-generate report @@ -1231,6 +1231,24 @@ chrome.storage.local.get(['platform'], function (result) { // Update UI for platform function updatePlatformUI(platform) { + // Hide GitHub-specific settings for GitLab using the 'hidden' class + const orgSection = document.querySelector('.orgSection'); + if (orgSection) { + if (platform === 'gitlab') { + orgSection.classList.add('hidden'); + } else { + orgSection.classList.remove('hidden'); + } + } + // Hide all githubOnlySection elements for GitLab + const githubOnlySections = document.querySelectorAll('.githubOnlySection'); + githubOnlySections.forEach(el => { + if (platform === 'gitlab') { + el.classList.add('hidden'); + } else { + el.classList.remove('hidden'); + } + }); // (Optional) You can update the label/placeholder here if you want // Do NOT clear the username field here, only do it on actual platform change } From 09ebf95033cb16d0a2a4ab46c8d9c680d734d1cf Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 16 Jul 2025 16:49:44 +0530 Subject: [PATCH 31/40] fixed bug --- src/scripts/scrumHelper.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index e3b1715..02d23ab 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -52,6 +52,7 @@ function allIncluded(outputTarget = 'email') { let showOpenLabel = true; let showCommits = false; let userReason = ''; + let subjectForEmail = null; let pr_open_button = '

    open
    '; @@ -144,10 +145,10 @@ function allIncluded(outputTarget = 'email') { - + if (platform === 'github') { if (platformUsernameLocal) { - + fetchGithubData(); } else { if (outputTarget === 'popup') { @@ -219,9 +220,10 @@ function allIncluded(outputTarget = 'email') { if (date < 10) date = '0' + date; let dateCode = year.toString() + month.toString() + date.toString(); const subject = `[Scrum] ${name} - ${project} - ${dateCode}`; + subjectForEmail = subject; - await processGithubData(mappedData, true, subject); + await processGithubData(mappedData, true, subjectForEmail); scrumGenerationInProgress = false; } catch (err) { console.error('GitLab fetch failed:', err); @@ -876,8 +878,8 @@ function allIncluded(outputTarget = 'email') { } await writeGithubPrsReviews(); log('[DEBUG] Both data processing functions completed, generating scrum body'); - if (injectEmailTogether && subjectForEmail) { - // Synchronized subject and body injection for GitLab email + if (subjectForEmail) { + // Synchronized subject and body injection for email let lastWeekUl = '
      '; for (let i = 0; i < lastWeekArray.length; i++) lastWeekUl += lastWeekArray[i]; for (let i = 0; i < reviewedPrsArray.length; i++) lastWeekUl += reviewedPrsArray[i]; @@ -1174,11 +1176,11 @@ ${userReason}`; async function writeGithubIssuesPrs(items) { if (!items) { - + return; } if (!items.length) { - + return; } const headers = { 'Accept': 'application/vnd.github.v3+json' }; @@ -1682,15 +1684,15 @@ async function fetchUserRepositories(username, token, org = '') { updatedAt: repo.pushedAt, stars: repo.stargazerCount })); - + return repos.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); } catch (err) { - + throw err; } } catch (err) { - + throw err; } From aa76c995f886682d9ff58789b765eb6fd59a8977 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Wed, 16 Jul 2025 18:09:53 +0530 Subject: [PATCH 32/40] refresh cache for gitlab --- src/scripts/popup.js | 17 +++++++++++--- src/scripts/scrumHelper.js | 45 +++++++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/scripts/popup.js b/src/scripts/popup.js index 86ffa25..fcd7d51 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -1479,6 +1479,7 @@ document.querySelectorAll('input[name="timeframe"]').forEach(radio => { }); // refresh cache button + document.getElementById('refreshCache').addEventListener('click', async function () { const button = this; const originalText = button.innerHTML; @@ -1488,9 +1489,19 @@ document.getElementById('refreshCache').addEventListener('click', async function button.disabled = true; try { - // Clear both caches + // Determine platform + let platform = 'github'; + try { + const items = await new Promise(resolve => { + chrome.storage.local.get(['platform'], resolve); + }); + platform = items.platform || 'github'; + } catch (e) { } + + // Clear all caches + const keysToRemove = ['githubCache', 'repoCache', 'gitlabCache']; await new Promise(resolve => { - chrome.storage.local.remove(['githubCache', 'repoCache'], resolve); + chrome.storage.local.remove(keysToRemove, resolve); }); // Clear the scrum report @@ -1511,7 +1522,7 @@ document.getElementById('refreshCache').addEventListener('click', async function button.innerHTML = 'Cache Cleared!'; button.classList.remove('loading'); - setTimeout(() => triggerRepoFetchIfEnabled(), 500); + // Do NOT trigger report generation automatically setTimeout(() => { button.innerHTML = originalText; diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 02d23ab..887c361 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -25,6 +25,10 @@ let platformUsername = ''; let gitlabHelper = null; function allIncluded(outputTarget = 'email') { + // Always re-instantiate gitlabHelper for gitlab platform to ensure fresh cache after refresh + if (platform === 'gitlab' || (typeof platform === 'undefined' && window.GitLabHelper)) { + gitlabHelper = new window.GitLabHelper(); + } if (scrumGenerationInProgress) { return; } @@ -1473,6 +1477,26 @@ async function forceGithubDataRefresh() { return { success: true }; } +async function forceGitlabDataRefresh() { + // Clear in-memory cache if gitlabHelper is loaded + if (window.GitLabHelper && gitlabHelper instanceof window.GitLabHelper) { + gitlabHelper.cache.data = null; + gitlabHelper.cache.cacheKey = null; + gitlabHelper.cache.timestamp = 0; + gitlabHelper.cache.fetching = false; + gitlabHelper.cache.queue = []; + } + await new Promise(resolve => { + chrome.storage.local.remove('gitlabCache', resolve); + }); + hasInjectedContent = false; + // Re-instantiate gitlabHelper to ensure a fresh instance for next API call + if (window.GitLabHelper) { + gitlabHelper = new window.GitLabHelper(); + } + return { success: true }; +} + if (window.location.protocol.startsWith('http')) { allIncluded('email'); @@ -1487,11 +1511,22 @@ window.generateScrumReport = function () { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'forceRefresh') { - forceGithubDataRefresh() - .then(result => sendResponse(result)).catch(err => { - console.error('Force refresh failed:', err); - sendResponse({ success: false, error: err.message }); - }); + chrome.storage.local.get(['platform'], async (result) => { + const platform = result.platform || 'github'; + if (platform === 'gitlab') { + forceGitlabDataRefresh() + .then(result => sendResponse(result)).catch(err => { + console.error('Force refresh failed:', err); + sendResponse({ success: false, error: err.message }); + }); + } else { + forceGithubDataRefresh() + .then(result => sendResponse(result)).catch(err => { + console.error('Force refresh failed:', err); + sendResponse({ success: false, error: err.message }); + }); + } + }); return true; } }); From 439c8218db41a840c8673831eb3cb6e9b8948837 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 17 Jul 2025 16:04:30 +0530 Subject: [PATCH 33/40] added space --- src/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.html b/src/popup.html index 8c174da..eace3b6 100644 --- a/src/popup.html +++ b/src/popup.html @@ -157,7 +157,7 @@

      Your Username

    -
    +

    From a334a7c3325877c4747b93b386a5b5dd6001d83d Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Thu, 17 Jul 2025 16:08:45 +0530 Subject: [PATCH 34/40] toolkit edited --- src/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.html b/src/popup.html index eace3b6..5eae063 100644 --- a/src/popup.html +++ b/src/popup.html @@ -175,7 +175,7 @@

    Your Username

    - Github Token required, add it in the settings(for github platform) + Github Token required, add it in the settings.
    From 6ff01212a5d3db3cbae0b4ccdb3f7c61f2494585 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Fri, 18 Jul 2025 11:22:55 +0530 Subject: [PATCH 35/40] correctly fetching group actvities --- src/scripts/gitlabHelper.js | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/scripts/gitlabHelper.js b/src/scripts/gitlabHelper.js index da1add2..5ff9698 100644 --- a/src/scripts/gitlabHelper.js +++ b/src/scripts/gitlabHelper.js @@ -111,17 +111,33 @@ class GitLabHelper { throw new Error(`GitLab user '${username}' not found`); } const userId = users[0].id; - // Fetch user's projects - const projectsUrl = `${this.baseUrl}/users/${userId}/projects?per_page=100&order_by=updated_at&sort=desc`; - const projectsRes = await fetch(projectsUrl); - if (!projectsRes.ok) { - throw new Error(`Error fetching GitLab projects: ${projectsRes.status} ${projectsRes.statusText}`); + + // Fetch all projects the user is a member of (including group projects) + const membershipProjectsUrl = `${this.baseUrl}/users/${userId}/projects?membership=true&per_page=100&order_by=updated_at&sort=desc`; + const membershipProjectsRes = await fetch(membershipProjectsUrl); + if (!membershipProjectsRes.ok) { + throw new Error(`Error fetching GitLab membership projects: ${membershipProjectsRes.status} ${membershipProjectsRes.statusText}`); + } + const membershipProjects = await membershipProjectsRes.json(); + + // Fetch all projects the user has contributed to (public, group, etc.) + const contributedProjectsUrl = `${this.baseUrl}/users/${userId}/contributed_projects?per_page=100&order_by=updated_at&sort=desc`; + const contributedProjectsRes = await fetch(contributedProjectsUrl); + if (!contributedProjectsRes.ok) { + throw new Error(`Error fetching GitLab contributed projects: ${contributedProjectsRes.status} ${contributedProjectsRes.statusText}`); + } + const contributedProjects = await contributedProjectsRes.json(); + + // Merge and deduplicate projects by project id + const allProjectsMap = new Map(); + for (const p of [...membershipProjects, ...contributedProjects]) { + allProjectsMap.set(p.id, p); } - const projects = await projectsRes.json(); + const allProjects = Array.from(allProjectsMap.values()); // Fetch merge requests from each project (works without auth for public projects) let allMergeRequests = []; - for (const project of projects) { + for (const project of allProjects) { try { const projectMRsUrl = `${this.baseUrl}/projects/${project.id}/merge_requests?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; const projectMRsRes = await fetch(projectMRsUrl); @@ -139,7 +155,7 @@ class GitLabHelper { // Fetch issues from each project (works without auth for public projects) let allIssues = []; - for (const project of projects) { + for (const project of allProjects) { try { const projectIssuesUrl = `${this.baseUrl}/projects/${project.id}/issues?author_id=${userId}&created_after=${startDate}T00:00:00Z&created_before=${endDate}T23:59:59Z&per_page=100&order_by=updated_at&sort=desc`; const projectIssuesRes = await fetch(projectIssuesUrl); @@ -157,7 +173,7 @@ class GitLabHelper { const gitlabData = { user: users[0], - projects: projects, + projects: allProjects, mergeRequests: allMergeRequests, // use project-by-project response issues: allIssues, // use project-by-project response comments: [] // Empty array since we're not fetching comments From 72f95b0224fe6066c72987615c1c44c316999d34 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Fri, 18 Jul 2025 13:28:57 +0530 Subject: [PATCH 36/40] open label added --- src/scripts/scrumHelper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 887c361..9ebd9da 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -200,7 +200,7 @@ function allIncluded(outputTarget = 'email') { : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), number: item.iid, title: item.title, - state: item.state, + state: (type === 'issue' && item.state === 'opened') ? 'open' : item.state, project: repoName, pull_request: type === 'mr', }; @@ -259,7 +259,7 @@ function allIncluded(outputTarget = 'email') { : (item.web_url || (project ? `${project.web_url}/-/merge_requests/${item.iid}` : '')), number: item.iid, title: item.title, - state: item.state, + state: (type === 'issue' && item.state === 'opened') ? 'open' : item.state, project: repoName, pull_request: type === 'mr', }; From 24f69b25af37191a2c49edf3359459c4a5284fc0 Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Sat, 19 Jul 2025 12:10:49 +0530 Subject: [PATCH 37/40] username is persisted --- src/index.css | 18 +++- src/scripts/main.js | 30 +++++- src/scripts/popup.js | 183 +++++++++++++++++++++++++++---------- src/scripts/scrumHelper.js | 27 ++++-- 4 files changed, 194 insertions(+), 64 deletions(-) diff --git a/src/index.css b/src/index.css index 0555c50..2a22f80 100644 --- a/src/index.css +++ b/src/index.css @@ -712,10 +712,24 @@ hr, z-index: 1000 !important; } +/* Fix platform dropdown highlight in dark mode */ .dark-mode #platformDropdownList li { color: #ffffff !important; + width: 100%; + display: flex; + align-items: center; + cursor: pointer; + padding: 8px 16px !important; + box-sizing: border-box; + border-radius: 0.75rem; + background: transparent; + transition: background 0.2s; } -.dark-mode #platformDropdownList li:hover { - background-color: #374151 !important; +/* Remove hover background color for platform dropdown in dark mode */ +.dark-mode #platformDropdownList li:hover, +.dark-mode #platformDropdownList li.selected, +.dark-mode #platformDropdownList li[aria-selected="true"] { + background-color: transparent !important; + color: #fff !important; } \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js index b131bf0..30d060f 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -12,9 +12,23 @@ let userReasonElement = null; // userReason element removed from UI let showCommitsElement = document.getElementById('showCommits'); function handleBodyOnLoad() { + // Migration: Handle existing users with old platformUsername storage + chrome.storage.local.get(['platform', 'platformUsername'], function (result) { + if (result.platformUsername && result.platform) { + // Migrate old platformUsername to platform-specific storage + const platformUsernameKey = `${result.platform}Username`; + chrome.storage.local.set({ [platformUsernameKey]: result.platformUsername }); + // Remove the old key + chrome.storage.local.remove(['platformUsername']); + console.log(`[MIGRATION] Migrated platformUsername to ${platformUsernameKey}`); + } + }); + chrome.storage.local.get( [ - 'platformUsername', + 'platform', + 'githubUsername', + 'gitlabUsername', 'projectName', 'enableToggle', 'startingDate', @@ -28,9 +42,13 @@ function handleBodyOnLoad() { 'showCommits', ], (items) => { - if (items.platformUsername) { - platformUsernameElement.value = items.platformUsername; + // Load platform-specific username + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + if (items[platformUsernameKey]) { + platformUsernameElement.value = items[platformUsernameKey]; } + if (items.githubToken) { githubTokenElement.value = items.githubToken; } @@ -214,7 +232,11 @@ function getToday() { function handlePlatformUsernameChange() { let value = platformUsernameElement.value; - chrome.storage.local.set({ platformUsername: value }); + chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + chrome.storage.local.set({ [platformUsernameKey]: value }); + }); } function handleGithubTokenChange() { let value = githubTokenElement.value; diff --git a/src/scripts/popup.js b/src/scripts/popup.js index fcd7d51..795e88a 100644 --- a/src/scripts/popup.js +++ b/src/scripts/popup.js @@ -282,6 +282,18 @@ document.addEventListener('DOMContentLoaded', function () { }); function initializePopup() { + // Migration: Handle existing users with old platformUsername storage + chrome.storage.local.get(['platform', 'platformUsername'], function (result) { + if (result.platformUsername && result.platform) { + // Migrate old platformUsername to platform-specific storage + const platformUsernameKey = `${result.platform}Username`; + chrome.storage.local.set({ [platformUsernameKey]: result.platformUsername }); + // Remove the old key + chrome.storage.local.remove(['platformUsername']); + console.log(`[MIGRATION] Migrated platformUsername to ${platformUsernameKey}`); + } + }); + // Restore all persistent fields immediately on DOMContentLoaded const projectNameInput = document.getElementById('projectName'); const orgInput = document.getElementById('orgInput'); @@ -299,12 +311,16 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.get([ 'projectName', 'orgName', 'userReason', 'showOpenLabel', 'showCommits', 'githubToken', 'cacheInput', - 'enableToggle', 'lastWeekContribution', 'yesterdayContribution', 'startingDate', 'endingDate', 'selectedTimeframe', 'platform', 'platformUsername' + 'enableToggle', 'lastWeekContribution', 'yesterdayContribution', 'startingDate', 'endingDate', 'selectedTimeframe', 'platform', 'githubUsername', 'gitlabUsername' ], function (result) { if (result.projectName) projectNameInput.value = result.projectName; if (result.orgName) orgInput.value = result.orgName; if (result.userReason) userReasonInput.value = result.userReason; - if (typeof result.showOpenLabel !== 'undefined') showOpenLabelCheckbox.checked = result.showOpenLabel; + if (typeof result.showOpenLabel !== 'undefined') { + showOpenLabelCheckbox.checked = result.showOpenLabel; + } else { + showOpenLabelCheckbox.checked = true; // Default to true for new users + } if (typeof result.showCommits !== 'undefined') showCommitsCheckbox.checked = result.showCommits; if (result.githubToken) githubTokenInput.value = result.githubToken; if (result.cacheInput) cacheInput.value = result.cacheInput; @@ -319,7 +335,11 @@ document.addEventListener('DOMContentLoaded', function () { if (typeof result.yesterdayContribution !== 'undefined') yesterdayRadio.checked = result.yesterdayContribution; if (result.startingDate) startingDateInput.value = result.startingDate; if (result.endingDate) endingDateInput.value = result.endingDate; - platformUsername.value = result.platformUsername || ''; + + // Load platform-specific username + const platform = result.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + platformUsername.value = result[platformUsernameKey] || ''; }); // Button setup @@ -327,19 +347,24 @@ document.addEventListener('DOMContentLoaded', function () { const copyBtn = document.getElementById('copyReport'); generateBtn.addEventListener('click', function () { - chrome.storage.local.set({ - platform: platformSelect.value, - platformUsername: platformUsername.value - }, () => { - let org = orgInput.value.trim().toLowerCase(); - chrome.storage.local.set({ orgName: org }, () => { - // Reload platform from storage before generating report - chrome.storage.local.get(['platform'], function (res) { - platformSelect.value = res.platform || 'github'; - updatePlatformUI(platformSelect.value); - generateBtn.innerHTML = ' Generating...'; - generateBtn.disabled = true; - window.generateScrumReport && window.generateScrumReport(); + chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + + chrome.storage.local.set({ + platform: platformSelect.value, + [platformUsernameKey]: platformUsername.value + }, () => { + let org = orgInput.value.trim().toLowerCase(); + chrome.storage.local.set({ orgName: org }, () => { + // Reload platform from storage before generating report + chrome.storage.local.get(['platform'], function (res) { + platformSelect.value = res.platform || 'github'; + updatePlatformUI(platformSelect.value); + generateBtn.innerHTML = ' Generating...'; + generateBtn.disabled = true; + window.generateScrumReport && window.generateScrumReport(); + }); }); }); }); @@ -497,7 +522,11 @@ document.addEventListener('DOMContentLoaded', function () { // Save username to storage on input platformUsername.addEventListener('input', function () { - chrome.storage.local.set({ platformUsername: platformUsername.value }); + chrome.storage.local.get(['platform'], function (result) { + const platform = result.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + chrome.storage.local.set({ [platformUsernameKey]: platformUsername.value }); + }); }); @@ -608,10 +637,14 @@ document.addEventListener('DOMContentLoaded', function () { chrome.storage.local.get(['repoCache'], resolve); }); const items = await new Promise(resolve => { - chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve); + chrome.storage.local.get(['platform', 'githubUsername', 'githubToken', 'orgName'], resolve); }); - if (!items.platformUsername) { + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = items[platformUsernameKey]; + + if (!username) { if (repoStatus) { repoStatus.textContent = 'Username required'; } @@ -620,7 +653,7 @@ document.addEventListener('DOMContentLoaded', function () { if (window.fetchUserRepositories) { const repos = await window.fetchUserRepositories( - items.platformUsername, + username, items.githubToken, items.orgName || '' ); @@ -631,7 +664,7 @@ document.addEventListener('DOMContentLoaded', function () { repoStatus.textContent = `${repos.length} repositories loaded`; } - const repoCacheKey = `repos-${items.platformUsername}-${items.orgName || ''}`; + const repoCacheKey = `repos-${username}-${items.orgName || ''}`; chrome.storage.local.set({ repoCache: { data: repos, @@ -719,16 +752,20 @@ document.addEventListener('DOMContentLoaded', function () { }); const items = await new Promise(resolve => { - chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve); + chrome.storage.local.get(['platform', 'githubUsername', 'githubToken', 'orgName'], resolve); }); - if (!items.platformUsername) { + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = items[platformUsernameKey]; + + if (!username) { repoStatus.textContent = 'Github Username required'; return; } - const repoCacheKey = `repos-${items.platformUsername}-${items.orgName || ''}`; + const repoCacheKey = `repos-${username}-${items.orgName || ''}`; const now = Date.now(); const cacheAge = cacheData.repoCache?.timestamp ? now - cacheData.repoCache.timestamp : Infinity; @@ -751,7 +788,7 @@ document.addEventListener('DOMContentLoaded', function () { if (window.fetchUserRepositories) { const repos = await window.fetchUserRepositories( - items.platformUsername, + username, items.githubToken, items.orgName || '', @@ -842,9 +879,12 @@ document.addEventListener('DOMContentLoaded', function () { }); function debugRepoFetch() { - chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], (items) => { + chrome.storage.local.get(['platform', 'githubUsername', 'githubToken', 'orgName'], (items) => { + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = items[platformUsernameKey]; console.log('Current settings:', { - username: items.platformUsername, + username: username, hasToken: !!items.githubToken, org: items.orgName || '' }); @@ -872,15 +912,18 @@ document.addEventListener('DOMContentLoaded', function () { return; } - chrome.storage.local.get(['platformUsername', 'githubToken'], (items) => { + chrome.storage.local.get(['platform', 'githubUsername', 'githubToken'], (items) => { + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = items[platformUsernameKey]; console.log('Storage data for repo fetch:', { - hasUsername: !!items.platformUsername, + hasUsername: !!username, hasToken: !!items.githubToken, - username: items.platformUsername + username: username }); - if (!items.platformUsername) { + if (!username) { repoStatus.textContent = 'Username required'; return; @@ -913,10 +956,13 @@ document.addEventListener('DOMContentLoaded', function () { }); const storageItems = await new Promise(resolve => { - chrome.storage.local.get(['platformUsername', 'githubToken', 'orgName'], resolve) + chrome.storage.local.get(['platform', 'githubUsername', 'githubToken', 'orgName'], resolve) }) - const repoCacheKey = `repos-${storageItems.platformUsername}-${storageItems.orgName || ''}`; + const platform = storageItems.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = storageItems[platformUsernameKey]; + const repoCacheKey = `repos-${username}-${storageItems.orgName || ''}`; const now = Date.now(); const cacheAge = cacheData.repoCache?.timestamp ? now - cacheData.repoCache.timestamp : Infinity; const cacheTTL = 10 * 60 * 1000; // 10 minutes @@ -945,7 +991,7 @@ document.addEventListener('DOMContentLoaded', function () { console.log('[POPUP-DEBUG] No valid cache. Fetching from network.'); availableRepos = await window.fetchUserRepositories( - storageItems.platformUsername, + username, storageItems.githubToken, storageItems.orgName || '' @@ -1094,8 +1140,11 @@ document.addEventListener('DOMContentLoaded', function () { window.removeRepo = removeRepo; - chrome.storage.local.get(['platformUsername'], (items) => { - if (items.platformUsername && useRepoFilter.checked && availableRepos.length === 0) { + chrome.storage.local.get(['platform', 'githubUsername'], (items) => { + const platform = items.platform || 'github'; + const platformUsernameKey = `${platform}Username`; + const username = items[platformUsernameKey]; + if (username && useRepoFilter.checked && availableRepos.length === 0) { setTimeout(() => loadRepos(), 1000); } @@ -1294,12 +1343,23 @@ function updatePlatformUI(platform) { platformSelect.addEventListener('change', function () { const platform = platformSelect.value; chrome.storage.local.set({ platform }); - // Clear username field and storage on platform change + // Save current username for current platform before switching const platformUsername = document.getElementById('platformUsername'); if (platformUsername) { - platformUsername.value = ''; - chrome.storage.local.set({ platformUsername: '' }); + const currentPlatform = platformSelect.value === 'github' ? 'gitlab' : 'github'; // Get the platform we're switching from + const currentUsername = platformUsername.value; + if (currentUsername.trim()) { + chrome.storage.local.set({ [`${currentPlatform}Username`]: currentUsername }); + } } + + // Load username for the new platform + chrome.storage.local.get([`${platform}Username`], function (result) { + if (platformUsername) { + platformUsername.value = result[`${platform}Username`] || ''; + } + }); + updatePlatformUI(platform); }); @@ -1316,14 +1376,27 @@ function setPlatformDropdown(value) { } else { dropdownSelected.innerHTML = ' GitHub'; } - platformSelectHidden.value = value; - chrome.storage.local.set({ platform: value }); - // Always clear username when user changes platform + + // Save current username for current platform before switching const platformUsername = document.getElementById('platformUsername'); if (platformUsername) { - platformUsername.value = ''; - chrome.storage.local.set({ platformUsername: '' }); + const currentPlatform = platformSelectHidden.value; + const currentUsername = platformUsername.value; + if (currentUsername.trim()) { + chrome.storage.local.set({ [`${currentPlatform}Username`]: currentUsername }); + } } + + platformSelectHidden.value = value; + chrome.storage.local.set({ platform: value }); + + // Load username for the new platform + chrome.storage.local.get([`${value}Username`], function (result) { + if (platformUsername) { + platformUsername.value = result[`${value}Username`] || ''; + } + }); + updatePlatformUI(value); } @@ -1338,10 +1411,15 @@ dropdownList.querySelectorAll('li').forEach(item => { const newPlatform = this.getAttribute('data-value'); const currentPlatform = platformSelectHidden.value; - // Only clear username if platform is actually changing + // Save current username for current platform before switching if (newPlatform !== currentPlatform) { - platformUsername.value = ''; - chrome.storage.local.set({ platformUsername: '' }); + const platformUsername = document.getElementById('platformUsername'); + if (platformUsername) { + const currentUsername = platformUsername.value; + if (currentUsername.trim()) { + chrome.storage.local.set({ [`${currentPlatform}Username`]: currentUsername }); + } + } } setPlatformDropdown(newPlatform); @@ -1380,10 +1458,15 @@ dropdownList.querySelectorAll('li').forEach((item, idx, arr) => { const newPlatform = this.getAttribute('data-value'); const currentPlatform = platformSelectHidden.value; - // Only clear username if platform is actually changing + // Save current username for current platform before switching if (newPlatform !== currentPlatform) { - platformUsername.value = ''; - chrome.storage.local.set({ platformUsername: '' }); + const platformUsername = document.getElementById('platformUsername'); + if (platformUsername) { + const currentUsername = platformUsername.value; + if (currentUsername.trim()) { + chrome.storage.local.set({ [`${currentPlatform}Username`]: currentUsername }); + } + } } setPlatformDropdown(newPlatform); diff --git a/src/scripts/scrumHelper.js b/src/scripts/scrumHelper.js index 9ebd9da..e045130 100644 --- a/src/scripts/scrumHelper.js +++ b/src/scripts/scrumHelper.js @@ -77,7 +77,8 @@ function allIncluded(outputTarget = 'email') { chrome.storage.local.get( [ 'platform', - 'platformUsername', + 'githubUsername', + 'gitlabUsername', 'githubToken', 'projectName', 'enableToggle', @@ -98,7 +99,10 @@ function allIncluded(outputTarget = 'email') { console.log("[DEBUG] Storage items received:", items); platform = items.platform || 'github'; - platformUsername = items.platformUsername || ''; + + // Load platform-specific username + const platformUsernameKey = `${platform}Username`; + platformUsername = items[platformUsernameKey] || ''; platformUsernameLocal = platformUsername; console.log(`[DEBUG] platform: ${platform}, platformUsername: ${platformUsername}`); @@ -106,11 +110,17 @@ function allIncluded(outputTarget = 'email') { const usernameFromDOM = document.getElementById('platformUsername')?.value; const projectFromDOM = document.getElementById('projectName')?.value; const tokenFromDOM = document.getElementById('githubToken')?.value; - items.platformUsername = usernameFromDOM || items.platformUsername; + + // Save to platform-specific storage + if (usernameFromDOM) { + chrome.storage.local.set({ [platformUsernameKey]: usernameFromDOM }); + platformUsername = usernameFromDOM; + platformUsernameLocal = usernameFromDOM; + } + items.projectName = projectFromDOM || items.projectName; items.githubToken = tokenFromDOM || items.githubToken; chrome.storage.local.set({ - platformUsername: items.platformUsername, projectName: items.projectName, githubToken: items.githubToken }); @@ -127,6 +137,7 @@ function allIncluded(outputTarget = 'email') { } showCommits = items.showCommits || false; + showOpenLabel = items.showOpenLabel !== false; // Default to true if not explicitly set to false orgName = items.orgName || ''; if (items.lastWeekContribution) { @@ -1283,9 +1294,9 @@ ${userReason}`; if (isDraft) { - li = `
  • (${project}) - Made PR (#${number}) - ${title} ${pr_draft_button}
  • `; + li = `
  • (${project}) - Made PR (#${number}) - ${title}${showOpenLabel ? ' ' + pr_draft_button : ''}
  • `; } else if (item.state === 'open' || item.state === 'opened') { - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_open_button}`; + li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_open_button : ''}`; if (showCommits && item._allCommits && item._allCommits.length && !isNewPR) { log(`[PR DEBUG] Rendering commits for existing PR #${number}:`, item._allCommits); @@ -1295,8 +1306,8 @@ ${userReason}`; } li += `
  • `; } else if (platform === 'gitlab' && item.state === 'closed') { - // For GitLab, always show closed label for closed MRs - li = `
  • (${project}) - ${prAction} (#${number}) - ${title} ${pr_closed_button}
  • `; + // For GitLab, show closed label for closed MRs only if showOpenLabel is enabled + li = `
  • (${project}) - ${prAction} (#${number}) - ${title}${showOpenLabel ? ' ' + pr_closed_button : ''}
  • `; } else { // GitHub: check merged status if possible let merged = null; From 14243d6e63de618d71b65f494c204bb31696c55a Mon Sep 17 00:00:00 2001 From: Preeti9764 Date: Mon, 21 Jul 2025 14:55:02 +0530 Subject: [PATCH 38/40] added language translation for new headers --- src/_locales/de/messages.json | 264 +++++++++++++++++++++++-------- src/_locales/en/messages.json | 8 + src/_locales/es/messages.json | 264 +++++++++++++++++++++++-------- src/_locales/fr/messages.json | 264 +++++++++++++++++++++++-------- src/_locales/hi/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/id/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/ja/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/pt/messages.json | 182 +++++++++++++++------ src/_locales/pt_BR/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/ru/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/vi/messages.json | 262 ++++++++++++++++++++++-------- src/_locales/zh_CN/messages.json | 264 +++++++++++++++++++++++-------- src/_locales/zh_TW/messages.json | 262 ++++++++++++++++++++++-------- src/popup.html | 31 ++-- 14 files changed, 2341 insertions(+), 770 deletions(-) diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json index ecde6ac..f4b05a1 100644 --- a/src/_locales/de/messages.json +++ b/src/_locales/de/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Berichten Sie über Ihren Entwicklungsfortschritt, indem Sie Ihre Git-Aktivität für einen ausgewählten Zeitraum automatisch abrufen" }, - "disableLabel": { "message": "Deaktivieren" }, - "enableLabel": { "message": "Aktivieren" }, - "homeButtonTitle": { "message": "Zum Bericht" }, - "projectNameLabel": { "message": "Ihr Projektname" }, - "projectNamePlaceholder": { "message": "Geben Sie Ihren Projektnamen ein" }, - "githubUsernameLabel": { "message": "Ihr GitHub-Benutzername" }, - "githubUsernamePlaceholder": { "message": "Erforderlich, um Ihre Beiträge abzurufen" }, - "contributionsLabel": { "message": "Ihre Beiträge abrufen für:" }, - "last7DaysLabel": { "message": "Letzte 7 Tage" }, - "last1DayLabel": { "message": "Gestern" }, - "startDateLabel": { "message": "Startdatum:" }, - "endDateLabel": { "message": "Enddatum:" }, - "showOpenClosedLabel": { "message": "Offen/Geschlossen-Status anzeigen" }, - "blockersLabel": { "message": "Was hindert Sie am Fortschritt?" }, - "blockersPlaceholder": { "message": "Geben Sie hier den Grund ein" }, - "scrumReportLabel": { "message": "Scrum-Bericht" }, - "generateReportButton": { "message": "Bericht erstellen" }, - "copyReportButton": { "message": "Bericht kopieren" }, - "settingsOrgNameLabel": { "message": "Name der Organisation" }, - "settingsOrgNamePlaceholder": { "message": "Namen der Organisation eingeben" }, - "setButton": { "message": "Festlegen" }, - "githubTokenLabel": { "message": "Ihr GitHub-Token" }, - "githubTokenPlaceholder": { "message": "Für authentifizierte Anfragen erforderlich" }, - "showCommitsLabel": { "message": "Commits in offenen PRs innerhalb des Zeitraums anzeigen" }, - "cacheTTLLabel": { "message": "Cache-TTL eingeben" }, - "cacheTTLUnit": { "message": "(in Minuten)" }, - "cacheTTLPlaceholder": { "message": "Cache-TTL in Minuten (Standard: 10)" }, - "refreshDataButton": { "message": "Daten aktualisieren (Cache umgehen)" }, - "refreshDataInfo": { "message": "Verwenden Sie diese Schaltfläche, um neue Daten sofort abzurufen." }, - "noteTitle": { "message": "Hinweis:" }, - "viewCodeButton": { "message": "Code ansehen" }, - "reportIssueButton": { "message": "Problem melden" }, - "repoFilterLabel": { "message": "Nach Repositories filtern" }, - "enableFilteringLabel": { "message": "Filterung aktivieren" }, - "tokenRequiredWarning": { "message": "Für die Repository-Filterung ist ein GitHub-Token erforderlich. Bitte fügen Sie einen in den Einstellungen hinzu." }, - "repoSearchPlaceholder": { "message": "Repository zum Hinzufügen suchen..." }, - "repoPlaceholder": { "message": "Keine Repositories ausgewählt (alle werden einbezogen)" }, - "repoCountNone": { "message": "0 Repositories ausgewählt" }, - "repoCount": { "message": "$1 Repositories ausgewählt" }, - "repoLoading": { "message": "Lade Repositories..." }, - "repoLoaded": { "message": "$1 Repositories geladen." }, - "repoNotFound": { "message": "Keine Repositories gefunden" }, - "repoRefetching": { "message": "Lade Repositories neu..." }, - "repoLoadFailed": { "message": "Laden der Repositories fehlgeschlagen." }, - "repoTokenPrivate": { "message": "Token ist ungültig oder hat keine Rechte für private Repos." }, - "repoRefetchFailed": { "message": "Neuladen der Repositories fehlgeschlagen." }, - "orgSetMessage": { "message": "Organisation erfolgreich festgelegt." }, - "orgClearedMessage": { "message": "Organisation gelöscht. Klicken Sie auf 'Bericht erstellen', um alle Aktivitäten abzurufen." }, - "orgNotFoundMessage": { "message": "Organisation auf GitHub nicht gefunden." }, - "orgValidationErrorMessage": { "message": "Fehler bei der Validierung der Organisation." }, - "refreshingButton": { "message": "Wird aktualisiert..." }, - "cacheClearFailed": { "message": "Cache-Löschung fehlgeschlagen." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Wird generiert..." }, - "copiedButton": { "message": "Kopiert!" }, - "orgChangedMessage": { "message": "Organisation geändert. Klicken Sie auf 'Bericht erstellen', um die GitHub-Aktivitäten abzurufen." }, - "cacheClearedMessage": { "message": "Cache erfolgreich geleert. Klicken Sie auf 'Bericht erstellen', um neue Daten abzurufen." }, - "cacheClearedButton": { "message": "Cache geleert!" }, - "extensionDisabledMessage": { "message": "Erweiterung ist deaktiviert. Aktivieren Sie sie in den Einstellungen, um Scrum-Berichte zu erstellen." }, - "notePoint1": { "message": "Die abgerufenen PRs basieren auf der letzten Überprüfung durch einen beliebigen Beitragenden. Wenn Sie einen PR vor 10 Tagen überprüft haben und jemand anderes ihn vor 2 Tagen, wird er trotzdem in Ihrer Aktivität der letzten Woche angezeigt." }, - "notePoint2": { "message": "Bitte beachten Sie, dass es im generierten Scrum zu Abweichungen kommen kann. Wir empfehlen, den Bericht vor dem Teilen manuell zu überprüfen, um die Genauigkeit sicherzustellen." }, - "noteSeeIssue": { "message": "Siehe dieses Issue" } -} + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Berichten Sie über Ihren Entwicklungsfortschritt, indem Sie Ihre Git-Aktivität für einen ausgewählten Zeitraum automatisch abrufen" + }, + "disableLabel": { + "message": "Deaktivieren" + }, + "enableLabel": { + "message": "Aktivieren" + }, + "homeButtonTitle": { + "message": "Zum Bericht" + }, + "projectNameLabel": { + "message": "Ihr Projektname" + }, + "projectNamePlaceholder": { + "message": "Geben Sie Ihren Projektnamen ein" + }, + "githubUsernameLabel": { + "message": "Ihr GitHub-Benutzername" + }, + "githubUsernamePlaceholder": { + "message": "Erforderlich, um Ihre Beiträge abzurufen" + }, + "contributionsLabel": { + "message": "Ihre Beiträge abrufen für:" + }, + "last7DaysLabel": { + "message": "Letzte 7 Tage" + }, + "last1DayLabel": { + "message": "Gestern" + }, + "startDateLabel": { + "message": "Startdatum:" + }, + "endDateLabel": { + "message": "Enddatum:" + }, + "showOpenClosedLabel": { + "message": "Offen/Geschlossen-Status anzeigen" + }, + "blockersLabel": { + "message": "Was hindert Sie am Fortschritt?" + }, + "blockersPlaceholder": { + "message": "Geben Sie hier den Grund ein" + }, + "scrumReportLabel": { + "message": "Scrum-Bericht" + }, + "generateReportButton": { + "message": "Bericht erstellen" + }, + "copyReportButton": { + "message": "Bericht kopieren" + }, + "settingsOrgNameLabel": { + "message": "Name der Organisation" + }, + "settingsOrgNamePlaceholder": { + "message": "Namen der Organisation eingeben" + }, + "setButton": { + "message": "Festlegen" + }, + "githubTokenLabel": { + "message": "Ihr GitHub-Token" + }, + "githubTokenPlaceholder": { + "message": "Für authentifizierte Anfragen erforderlich" + }, + "showCommitsLabel": { + "message": "Commits in offenen PRs innerhalb des Zeitraums anzeigen" + }, + "cacheTTLLabel": { + "message": "Cache-TTL eingeben" + }, + "cacheTTLUnit": { + "message": "(in Minuten)" + }, + "cacheTTLPlaceholder": { + "message": "Cache-TTL in Minuten (Standard: 10)" + }, + "refreshDataButton": { + "message": "Daten aktualisieren (Cache umgehen)" + }, + "refreshDataInfo": { + "message": "Verwenden Sie diese Schaltfläche, um neue Daten sofort abzurufen." + }, + "noteTitle": { + "message": "Hinweis:" + }, + "viewCodeButton": { + "message": "Code ansehen" + }, + "reportIssueButton": { + "message": "Problem melden" + }, + "repoFilterLabel": { + "message": "Nach Repositories filtern" + }, + "enableFilteringLabel": { + "message": "Filterung aktivieren" + }, + "tokenRequiredWarning": { + "message": "Für die Repository-Filterung ist ein GitHub-Token erforderlich. Bitte fügen Sie einen in den Einstellungen hinzu." + }, + "repoSearchPlaceholder": { + "message": "Repository zum Hinzufügen suchen..." + }, + "repoPlaceholder": { + "message": "Keine Repositories ausgewählt (alle werden einbezogen)" + }, + "repoCountNone": { + "message": "0 Repositories ausgewählt" + }, + "repoCount": { + "message": "$1 Repositories ausgewählt" + }, + "repoLoading": { + "message": "Lade Repositories..." + }, + "repoLoaded": { + "message": "$1 Repositories geladen." + }, + "repoNotFound": { + "message": "Keine Repositories gefunden" + }, + "repoRefetching": { + "message": "Lade Repositories neu..." + }, + "repoLoadFailed": { + "message": "Laden der Repositories fehlgeschlagen." + }, + "repoTokenPrivate": { + "message": "Token ist ungültig oder hat keine Rechte für private Repos." + }, + "repoRefetchFailed": { + "message": "Neuladen der Repositories fehlgeschlagen." + }, + "orgSetMessage": { + "message": "Organisation erfolgreich festgelegt." + }, + "orgClearedMessage": { + "message": "Organisation gelöscht. Klicken Sie auf 'Bericht erstellen', um alle Aktivitäten abzurufen." + }, + "orgNotFoundMessage": { + "message": "Organisation auf GitHub nicht gefunden." + }, + "orgValidationErrorMessage": { + "message": "Fehler bei der Validierung der Organisation." + }, + "refreshingButton": { + "message": "Wird aktualisiert..." + }, + "cacheClearFailed": { + "message": "Cache-Löschung fehlgeschlagen." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Wird generiert..." + }, + "copiedButton": { + "message": "Kopiert!" + }, + "orgChangedMessage": { + "message": "Organisation geändert. Klicken Sie auf 'Bericht erstellen', um die GitHub-Aktivitäten abzurufen." + }, + "cacheClearedMessage": { + "message": "Cache erfolgreich geleert. Klicken Sie auf 'Bericht erstellen', um neue Daten abzurufen." + }, + "cacheClearedButton": { + "message": "Cache geleert!" + }, + "extensionDisabledMessage": { + "message": "Erweiterung ist deaktiviert. Aktivieren Sie sie in den Einstellungen, um Scrum-Berichte zu erstellen." + }, + "notePoint1": { + "message": "Die abgerufenen PRs basieren auf der letzten Überprüfung durch einen beliebigen Beitragenden. Wenn Sie einen PR vor 10 Tagen überprüft haben und jemand anderes ihn vor 2 Tagen, wird er trotzdem in Ihrer Aktivität der letzten Woche angezeigt." + }, + "notePoint2": { + "message": "Bitte beachten Sie, dass es im generierten Scrum zu Abweichungen kommen kann. Wir empfehlen, den Bericht vor dem Teilen manuell zu überprüfen, um die Genauigkeit sicherzustellen." + }, + "noteSeeIssue": { + "message": "Siehe dieses Issue" + }, + "platformLabel": { + "message": "Plattform" + }, + "usernameLabel": { + "message": "Ihr Benutzername" + } +} \ No newline at end of file diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 148e822..007b7c1 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -254,5 +254,13 @@ "noteSeeIssue": { "message": "See this issue", "description": "Link text for the GitHub issue in the notes." + }, + "platformLabel": { + "message": "Platform", + "description": "Label for the platform selection header." + }, + "usernameLabel": { + "message": "Your Username", + "description": "Label for the username input header." } } \ No newline at end of file diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json index eb52d6b..cf0cdeb 100644 --- a/src/_locales/es/messages.json +++ b/src/_locales/es/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Informe sobre su progreso de desarrollo obteniendo automáticamente su actividad de Git para un período seleccionado." }, - "disableLabel": { "message": "Desactivar" }, - "enableLabel": { "message": "Activar" }, - "homeButtonTitle": { "message": "Ir al informe" }, - "projectNameLabel": { "message": "Nombre de su proyecto" }, - "projectNamePlaceholder": { "message": "Ingrese el nombre de su proyecto" }, - "githubUsernameLabel": { "message": "Su nombre de usuario de GitHub" }, - "githubUsernamePlaceholder": { "message": "Requerido para obtener sus contribuciones" }, - "contributionsLabel": { "message": "Obtener sus contribuciones de:" }, - "last7DaysLabel": { "message": "Últimos 7 días" }, - "last1DayLabel": { "message": "Ayer" }, - "startDateLabel": { "message": "Fecha de inicio:" }, - "endDateLabel": { "message": "Fecha de fin:" }, - "showOpenClosedLabel": { "message": "Mostrar estado Abierto/Cerrado" }, - "blockersLabel": { "message": "¿Qué le impide progresar?" }, - "blockersPlaceholder": { "message": "Ingrese su motivo" }, - "scrumReportLabel": { "message": "Informe de Scrum" }, - "generateReportButton": { "message": "Generar informe" }, - "copyReportButton": { "message": "Copiar informe" }, - "settingsOrgNameLabel": { "message": "Nombre de la organización" }, - "settingsOrgNamePlaceholder": { "message": "Ingrese el nombre de la organización" }, - "setButton": { "message": "Establecer" }, - "githubTokenLabel": { "message": "Su token de GitHub" }, - "githubTokenPlaceholder": { "message": "Requerido para realizar solicitudes autenticadas" }, - "showCommitsLabel": { "message": "Mostrar commits en PRs abiertos dentro del rango de fechas" }, - "cacheTTLLabel": { "message": "Ingresar TTL del caché" }, - "cacheTTLUnit": { "message": "(en minutos)" }, - "cacheTTLPlaceholder": { "message": "Escriba el TTL del caché en minutos (Predeterminado: 10)" }, - "refreshDataButton": { "message": "Actualizar datos (omitir caché)" }, - "refreshDataInfo": { "message": "Use este botón para obtener datos nuevos inmediatamente." }, - "noteTitle": { "message": "Nota:" }, - "viewCodeButton": { "message": "Ver código" }, - "reportIssueButton": { "message": "Reportar un problema" }, - "repoFilterLabel": { "message": "Filtrar por repositorios" }, - "enableFilteringLabel": { "message": "Activar filtrado" }, - "tokenRequiredWarning": { "message": "Se requiere un token de GitHub para filtrar repositorios. Por favor, agregue uno en la configuración." }, - "repoSearchPlaceholder": { "message": "Buscar un repositorio para agregar..." }, - "repoPlaceholder": { "message": "No hay repositorios seleccionados (se incluirán todos)" }, - "repoCountNone": { "message": "0 repositorios seleccionados" }, - "repoCount": { "message": "$1 repositorios seleccionados" }, - "repoLoading": { "message": "Cargando repositorios..." }, - "repoLoaded": { "message": "$1 repositorios cargados." }, - "repoNotFound": { "message": "No se encontraron repositorios" }, - "repoRefetching": { "message": "Recargando repositorios..." }, - "repoLoadFailed": { "message": "Error al cargar los repositorios." }, - "repoTokenPrivate": { "message": "El token no es válido o no tiene derechos para repositorios privados." }, - "repoRefetchFailed": { "message": "Error al recargar los repositorios." }, - "orgSetMessage": { "message": "Organización establecida correctamente." }, - "orgClearedMessage": { "message": "Organización eliminada. Haga clic en 'Generar informe' para obtener todas las actividades." }, - "orgNotFoundMessage": { "message": "Organización no encontrada en GitHub." }, - "orgValidationErrorMessage": { "message": "Error al validar la organización." }, - "refreshingButton": { "message": "Actualizando..." }, - "cacheClearFailed": { "message": "Error al borrar el caché." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Generando..." }, - "copiedButton": { "message": "¡Copiado!" }, - "orgChangedMessage": { "message": "Organización cambiada. Haga clic en 'Generar informe' para obtener las actividades de GitHub." }, - "cacheClearedMessage": { "message": "Caché borrado con éxito. Haga clic en 'Generar informe' para obtener datos nuevos." }, - "cacheClearedButton": { "message": "¡Caché borrado!" }, - "extensionDisabledMessage": { "message": "La extensión está desactivada. Actívela desde los ajustes para generar informes de scrum." }, - "notePoint1": { "message": "Los PRs se obtienen según la revisión más reciente de cualquier contribuyente. Si revisó un PR hace 10 días y otra persona lo hizo hace 2 días, seguirá apareciendo en su actividad de la última semana." }, - "notePoint2": { "message": "Tenga en cuenta que pueden ocurrir algunas discrepancias en el Scrum generado. Recomendamos revisar y editar manualmente el informe para garantizar la precisión antes de compartirlo." }, - "noteSeeIssue": { "message": "Ver este issue" } -} + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Informe sobre su progreso de desarrollo obteniendo automáticamente su actividad de Git para un período seleccionado." + }, + "disableLabel": { + "message": "Desactivar" + }, + "enableLabel": { + "message": "Activar" + }, + "homeButtonTitle": { + "message": "Ir al informe" + }, + "projectNameLabel": { + "message": "Nombre de su proyecto" + }, + "projectNamePlaceholder": { + "message": "Ingrese el nombre de su proyecto" + }, + "githubUsernameLabel": { + "message": "Su nombre de usuario de GitHub" + }, + "githubUsernamePlaceholder": { + "message": "Requerido para obtener sus contribuciones" + }, + "contributionsLabel": { + "message": "Obtener sus contribuciones de:" + }, + "last7DaysLabel": { + "message": "Últimos 7 días" + }, + "last1DayLabel": { + "message": "Ayer" + }, + "startDateLabel": { + "message": "Fecha de inicio:" + }, + "endDateLabel": { + "message": "Fecha de fin:" + }, + "showOpenClosedLabel": { + "message": "Mostrar estado Abierto/Cerrado" + }, + "blockersLabel": { + "message": "¿Qué le impide progresar?" + }, + "blockersPlaceholder": { + "message": "Ingrese su motivo" + }, + "scrumReportLabel": { + "message": "Informe de Scrum" + }, + "generateReportButton": { + "message": "Generar informe" + }, + "copyReportButton": { + "message": "Copiar informe" + }, + "settingsOrgNameLabel": { + "message": "Nombre de la organización" + }, + "settingsOrgNamePlaceholder": { + "message": "Ingrese el nombre de la organización" + }, + "setButton": { + "message": "Establecer" + }, + "githubTokenLabel": { + "message": "Su token de GitHub" + }, + "githubTokenPlaceholder": { + "message": "Requerido para realizar solicitudes autenticadas" + }, + "showCommitsLabel": { + "message": "Mostrar commits en PRs abiertos dentro del rango de fechas" + }, + "cacheTTLLabel": { + "message": "Ingresar TTL del caché" + }, + "cacheTTLUnit": { + "message": "(en minutos)" + }, + "cacheTTLPlaceholder": { + "message": "Escriba el TTL del caché en minutos (Predeterminado: 10)" + }, + "refreshDataButton": { + "message": "Actualizar datos (omitir caché)" + }, + "refreshDataInfo": { + "message": "Use este botón para obtener datos nuevos inmediatamente." + }, + "noteTitle": { + "message": "Nota:" + }, + "viewCodeButton": { + "message": "Ver código" + }, + "reportIssueButton": { + "message": "Reportar un problema" + }, + "repoFilterLabel": { + "message": "Filtrar por repositorios" + }, + "enableFilteringLabel": { + "message": "Activar filtrado" + }, + "tokenRequiredWarning": { + "message": "Se requiere un token de GitHub para filtrar repositorios. Por favor, agregue uno en la configuración." + }, + "repoSearchPlaceholder": { + "message": "Buscar un repositorio para agregar..." + }, + "repoPlaceholder": { + "message": "No hay repositorios seleccionados (se incluirán todos)" + }, + "repoCountNone": { + "message": "0 repositorios seleccionados" + }, + "repoCount": { + "message": "$1 repositorios seleccionados" + }, + "repoLoading": { + "message": "Cargando repositorios..." + }, + "repoLoaded": { + "message": "$1 repositorios cargados." + }, + "repoNotFound": { + "message": "No se encontraron repositorios" + }, + "repoRefetching": { + "message": "Recargando repositorios..." + }, + "repoLoadFailed": { + "message": "Error al cargar los repositorios." + }, + "repoTokenPrivate": { + "message": "El token no es válido o no tiene derechos para repositorios privados." + }, + "repoRefetchFailed": { + "message": "Error al recargar los repositorios." + }, + "orgSetMessage": { + "message": "Organización establecida correctamente." + }, + "orgClearedMessage": { + "message": "Organización eliminada. Haga clic en 'Generar informe' para obtener todas las actividades." + }, + "orgNotFoundMessage": { + "message": "Organización no encontrada en GitHub." + }, + "orgValidationErrorMessage": { + "message": "Error al validar la organización." + }, + "refreshingButton": { + "message": "Actualizando..." + }, + "cacheClearFailed": { + "message": "Error al borrar el caché." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Generando..." + }, + "copiedButton": { + "message": "¡Copiado!" + }, + "orgChangedMessage": { + "message": "Organización cambiada. Haga clic en 'Generar informe' para obtener las actividades de GitHub." + }, + "cacheClearedMessage": { + "message": "Caché borrado con éxito. Haga clic en 'Generar informe' para obtener datos nuevos." + }, + "cacheClearedButton": { + "message": "¡Caché borrado!" + }, + "extensionDisabledMessage": { + "message": "La extensión está desactivada. Actívela desde los ajustes para generar informes de scrum." + }, + "notePoint1": { + "message": "Los PRs se obtienen según la revisión más reciente de cualquier contribuyente. Si revisó un PR hace 10 días y otra persona lo hizo hace 2 días, seguirá apareciendo en su actividad de la última semana." + }, + "notePoint2": { + "message": "Tenga en cuenta que pueden ocurrir algunas discrepancias en el Scrum generado. Recomendamos revisar y editar manualmente el informe para garantizar la precisión antes de compartirlo." + }, + "noteSeeIssue": { + "message": "Ver este issue" + }, + "platformLabel": { + "message": "Plataforma" + }, + "usernameLabel": { + "message": "Su nombre de usuario" + } +} \ No newline at end of file diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json index 4e0e680..a75f55f 100644 --- a/src/_locales/fr/messages.json +++ b/src/_locales/fr/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Rendez compte de votre progression en récupérant automatiquement votre activité Git pour une période sélectionnée." }, - "disableLabel": { "message": "Désactiver" }, - "enableLabel": { "message": "Activer" }, - "homeButtonTitle": { "message": "Aller au rapport" }, - "projectNameLabel": { "message": "Nom de votre projet" }, - "projectNamePlaceholder": { "message": "Entrez le nom de votre projet" }, - "githubUsernameLabel": { "message": "Votre nom d'utilisateur GitHub" }, - "githubUsernamePlaceholder": { "message": "Requis pour récupérer vos contributions" }, - "contributionsLabel": { "message": "Récupérer vos contributions de :" }, - "last7DaysLabel": { "message": "7 derniers jours" }, - "last1DayLabel": { "message": "Hier" }, - "startDateLabel": { "message": "Date de début :" }, - "endDateLabel": { "message": "Date de fin :" }, - "showOpenClosedLabel": { "message": "Afficher le statut Ouvert/Fermé" }, - "blockersLabel": { "message": "Qu'est-ce qui vous empêche de progresser ?" }, - "blockersPlaceholder": { "message": "Entrez votre raison" }, - "scrumReportLabel": { "message": "Rapport Scrum" }, - "generateReportButton": { "message": "Générer le rapport" }, - "copyReportButton": { "message": "Copier le rapport" }, - "settingsOrgNameLabel": { "message": "Nom de l'organisation" }, - "settingsOrgNamePlaceholder": { "message": "Entrez le nom de l'organisation" }, - "setButton": { "message": "Définir" }, - "githubTokenLabel": { "message": "Votre jeton GitHub" }, - "githubTokenPlaceholder": { "message": "Requis pour les requêtes authentifiées" }, - "showCommitsLabel": { "message": "Afficher les commits dans les PR ouverts pour la période sélectionnée" }, - "cacheTTLLabel": { "message": "Entrer le TTL du cache" }, - "cacheTTLUnit": { "message": "(en minutes)" }, - "cacheTTLPlaceholder": { "message": "Entrez le TTL du cache en minutes (Défaut : 10)" }, - "refreshDataButton": { "message": "Actualiser les données (ignorer le cache)" }, - "refreshDataInfo": { "message": "Utilisez ce bouton pour récupérer des données fraîches immédiatement." }, - "noteTitle": { "message": "Note :" }, - "viewCodeButton": { "message": "Voir le code" }, - "reportIssueButton": { "message": "Signaler un problème" }, - "repoFilterLabel": { "message": "Filtrer par dépôts" }, - "enableFilteringLabel": { "message": "Activer le filtrage" }, - "tokenRequiredWarning": { "message": "Un jeton GitHub est requis pour filtrer les dépôts. Veuillez en ajouter un dans les paramètres." }, - "repoSearchPlaceholder": { "message": "Rechercher un dépôt à ajouter..." }, - "repoPlaceholder": { "message": "Aucun dépôt sélectionné (tous seront inclus)" }, - "repoCountNone": { "message": "0 dépôts sélectionnés" }, - "repoCount": { "message": "$1 dépôts sélectionnés" }, - "repoLoading": { "message": "Chargement des dépôts..." }, - "repoLoaded": { "message": "$1 dépôts chargés." }, - "repoNotFound": { "message": "Aucun dépôt trouvé" }, - "repoRefetching": { "message": "Rechargement des dépôts..." }, - "repoLoadFailed": { "message": "Échec du chargement des dépôts." }, - "repoTokenPrivate": { "message": "Le jeton est invalide ou n'a pas les droits pour les dépôts privés." }, - "repoRefetchFailed": { "message": "Échec du rechargement des dépôts." }, - "orgSetMessage": { "message": "Organisation définie avec succès." }, - "orgClearedMessage": { "message": "Organisation effacée. Cliquez sur 'Générer le rapport' pour récupérer toutes les activités." }, - "orgNotFoundMessage": { "message": "Organisation non trouvée sur GitHub." }, - "orgValidationErrorMessage": { "message": "Erreur lors de la validation de l'organisation." }, - "refreshingButton": { "message": "Actualisation..." }, - "cacheClearFailed": { "message": "Échec de la suppression du cache." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Génération..." }, - "copiedButton": { "message": "Copié !" }, - "orgChangedMessage": { "message": "Organisation modifiée. Cliquez sur 'Générer le rapport' pour récupérer les activités GitHub." }, - "cacheClearedMessage": { "message": "Cache vidé avec succès. Cliquez sur 'Générer le rapport' pour récupérer des données fraîches." }, - "cacheClearedButton": { "message": "Cache vidé !" }, - "extensionDisabledMessage": { "message": "L'extension est désactivée. Activez-la depuis les paramètres pour générer des rapports Scrum." }, - "notePoint1": { "message": "Les PR récupérés sont basés sur la révision la plus récente de n'importe quel contributeur. Si vous avez révisé un PR il y a 10 jours et qu'un autre l'a fait il y a 2 jours, il apparaîtra toujours dans votre activité de la semaine passée." }, - "notePoint2": { "message": "Veuillez noter que des divergences peuvent survenir dans le Scrum généré. Nous recommandons de réviser et de modifier manuellement le rapport pour garantir son exactitude avant de le partager." }, - "noteSeeIssue": { "message": "Voir ce ticket" } -} + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Rendez compte de votre progression en récupérant automatiquement votre activité Git pour une période sélectionnée." + }, + "disableLabel": { + "message": "Désactiver" + }, + "enableLabel": { + "message": "Activer" + }, + "homeButtonTitle": { + "message": "Aller au rapport" + }, + "projectNameLabel": { + "message": "Nom de votre projet" + }, + "projectNamePlaceholder": { + "message": "Entrez le nom de votre projet" + }, + "githubUsernameLabel": { + "message": "Votre nom d'utilisateur GitHub" + }, + "githubUsernamePlaceholder": { + "message": "Requis pour récupérer vos contributions" + }, + "contributionsLabel": { + "message": "Récupérer vos contributions de :" + }, + "last7DaysLabel": { + "message": "7 derniers jours" + }, + "last1DayLabel": { + "message": "Hier" + }, + "startDateLabel": { + "message": "Date de début :" + }, + "endDateLabel": { + "message": "Date de fin :" + }, + "showOpenClosedLabel": { + "message": "Afficher le statut Ouvert/Fermé" + }, + "blockersLabel": { + "message": "Qu'est-ce qui vous empêche de progresser ?" + }, + "blockersPlaceholder": { + "message": "Entrez votre raison" + }, + "scrumReportLabel": { + "message": "Rapport Scrum" + }, + "generateReportButton": { + "message": "Générer le rapport" + }, + "copyReportButton": { + "message": "Copier le rapport" + }, + "settingsOrgNameLabel": { + "message": "Nom de l'organisation" + }, + "settingsOrgNamePlaceholder": { + "message": "Entrez le nom de l'organisation" + }, + "setButton": { + "message": "Définir" + }, + "githubTokenLabel": { + "message": "Votre jeton GitHub" + }, + "githubTokenPlaceholder": { + "message": "Requis pour les requêtes authentifiées" + }, + "showCommitsLabel": { + "message": "Afficher les commits dans les PR ouverts pour la période sélectionnée" + }, + "cacheTTLLabel": { + "message": "Entrer le TTL du cache" + }, + "cacheTTLUnit": { + "message": "(en minutes)" + }, + "cacheTTLPlaceholder": { + "message": "Entrez le TTL du cache en minutes (Défaut : 10)" + }, + "refreshDataButton": { + "message": "Actualiser les données (ignorer le cache)" + }, + "refreshDataInfo": { + "message": "Utilisez ce bouton pour récupérer des données fraîches immédiatement." + }, + "noteTitle": { + "message": "Note :" + }, + "viewCodeButton": { + "message": "Voir le code" + }, + "reportIssueButton": { + "message": "Signaler un problème" + }, + "repoFilterLabel": { + "message": "Filtrer par dépôts" + }, + "enableFilteringLabel": { + "message": "Activer le filtrage" + }, + "tokenRequiredWarning": { + "message": "Un jeton GitHub est requis pour filtrer les dépôts. Veuillez en ajouter un dans les paramètres." + }, + "repoSearchPlaceholder": { + "message": "Rechercher un dépôt à ajouter..." + }, + "repoPlaceholder": { + "message": "Aucun dépôt sélectionné (tous seront inclus)" + }, + "repoCountNone": { + "message": "0 dépôts sélectionnés" + }, + "repoCount": { + "message": "$1 dépôts sélectionnés" + }, + "repoLoading": { + "message": "Chargement des dépôts..." + }, + "repoLoaded": { + "message": "$1 dépôts chargés." + }, + "repoNotFound": { + "message": "Aucun dépôt trouvé" + }, + "repoRefetching": { + "message": "Rechargement des dépôts..." + }, + "repoLoadFailed": { + "message": "Échec du chargement des dépôts." + }, + "repoTokenPrivate": { + "message": "Le jeton est invalide ou n'a pas les droits pour les dépôts privés." + }, + "repoRefetchFailed": { + "message": "Échec du rechargement des dépôts." + }, + "orgSetMessage": { + "message": "Organisation définie avec succès." + }, + "orgClearedMessage": { + "message": "Organisation effacée. Cliquez sur 'Générer le rapport' pour récupérer toutes les activités." + }, + "orgNotFoundMessage": { + "message": "Organisation non trouvée sur GitHub." + }, + "orgValidationErrorMessage": { + "message": "Erreur lors de la validation de l'organisation." + }, + "refreshingButton": { + "message": "Actualisation..." + }, + "cacheClearFailed": { + "message": "Échec de la suppression du cache." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Génération..." + }, + "copiedButton": { + "message": "Copié !" + }, + "orgChangedMessage": { + "message": "Organisation modifiée. Cliquez sur 'Générer le rapport' pour récupérer les activités GitHub." + }, + "cacheClearedMessage": { + "message": "Cache vidé avec succès. Cliquez sur 'Générer le rapport' pour récupérer des données fraîches." + }, + "cacheClearedButton": { + "message": "Cache vidé !" + }, + "extensionDisabledMessage": { + "message": "L'extension est désactivée. Activez-la depuis les paramètres pour générer des rapports Scrum." + }, + "notePoint1": { + "message": "Les PR récupérés sont basés sur la révision la plus récente de n'importe quel contributeur. Si vous avez révisé un PR il y a 10 jours et qu'un autre l'a fait il y a 2 jours, il apparaîtra toujours dans votre activité de la semaine passée." + }, + "notePoint2": { + "message": "Veuillez noter que des divergences peuvent survenir dans le Scrum généré. Nous recommandons de réviser et de modifier manuellement le rapport pour garantir son exactitude avant de le partager." + }, + "noteSeeIssue": { + "message": "Voir ce ticket" + }, + "platformLabel": { + "message": "Plateforme" + }, + "usernameLabel": { + "message": "Votre nom d'utilisateur" + } +} \ No newline at end of file diff --git a/src/_locales/hi/messages.json b/src/_locales/hi/messages.json index cace02e..92603a9 100644 --- a/src/_locales/hi/messages.json +++ b/src/_locales/hi/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "एक चयनित अवधि के लिए अपनी Git गतिविधि को स्वतः प्राप्त करके अपनी विकास प्रगति की रिपोर्ट करें।" }, - "disableLabel": { "message": "अक्षम करें" }, - "enableLabel": { "message": "सक्षम करें" }, - "homeButtonTitle": { "message": "रिपोर्ट पर जाएं" }, - "projectNameLabel": { "message": "आपकी परियोजना का नाम" }, - "projectNamePlaceholder": { "message": "अपनी परियोजना का नाम दर्ज करें" }, - "githubUsernameLabel": { "message": "आपका GitHub उपयोगकर्ता नाम" }, - "githubUsernamePlaceholder": { "message": "आपके योगदान प्राप्त करने के लिए आवश्यक" }, - "contributionsLabel": { "message": "इस अवधि के योगदान प्राप्त करें:" }, - "last7DaysLabel": { "message": "पिछले 7 दिन" }, - "last1DayLabel": { "message": "कल" }, - "startDateLabel": { "message": "प्रारंभ तिथि:" }, - "endDateLabel": { "message": "अंतिम तिथि:" }, - "showOpenClosedLabel": { "message": "खुला/बंद स्थिति दिखाएं" }, - "blockersLabel": { "message": "आपको प्रगति करने से क्या रोक रहा है?" }, - "blockersPlaceholder": { "message": "अपना कारण दर्ज करें" }, - "scrumReportLabel": { "message": "स्क्रम रिपोर्ट" }, - "generateReportButton": { "message": "रिपोर्ट बनाएं" }, - "copyReportButton": { "message": "रिपोर्ट कॉपी करें" }, - "settingsOrgNameLabel": { "message": "संगठन का नाम" }, - "settingsOrgNamePlaceholder": { "message": "संगठन का नाम दर्ज करें" }, - "setButton": { "message": "सेट करें" }, - "githubTokenLabel": { "message": "आपका GitHub टोकन" }, - "githubTokenPlaceholder": { "message": "प्रमाणित अनुरोधों के लिए आवश्यक" }, - "showCommitsLabel": { "message": "चयनित अवधि में खुले PRs में किए गए कमिट दिखाएं" }, - "cacheTTLLabel": { "message": "कैश TTL दर्ज करें" }, - "cacheTTLUnit": { "message": "(मिनटों में)" }, - "cacheTTLPlaceholder": { "message": "कैश TTL मिनटों में लिखें (डिफ़ॉल्ट: 10)" }, - "refreshDataButton": { "message": "डेटा रीफ्रेश करें (कैश को बायपास करें)" }, - "refreshDataInfo": { "message": "ताज़ा डेटा तुरंत प्राप्त करने के लिए इस बटन का उपयोग करें।" }, - "noteTitle": { "message": "ध्यान दें:" }, - "viewCodeButton": { "message": "कोड देखें" }, - "reportIssueButton": { "message": "समस्या की रिपोर्ट करें" }, - "repoFilterLabel": { "message": "रिपॉजिटरी द्वारा फ़िल्टर करें" }, - "enableFilteringLabel": { "message": "फ़िल्टरिंग सक्षम करें" }, - "tokenRequiredWarning": { "message": "रिपॉजिटरी फ़िल्टरिंग के लिए एक GitHub टोकन आवश्यक है। कृपया सेटिंग्स में एक जोड़ें।" }, - "repoSearchPlaceholder": { "message": "जोड़ने के लिए एक रिपॉजिटरी खोजें..." }, - "repoPlaceholder": { "message": "कोई रिपॉजिटरी चयनित नहीं (सभी शामिल होंगी)" }, - "repoCountNone": { "message": "0 रिपॉजिटरी चयनित" }, - "repoCount": { "message": "$1 रिपॉजिटरी चयनित" }, - "repoLoading": { "message": "रिपॉजिटरी लोड हो रही हैं..." }, - "repoLoaded": { "message": "$1 रिपॉजिटरी लोड हो गईं।" }, - "repoNotFound": { "message": "कोई रिपॉजिटरी नहीं मिली" }, - "repoRefetching": { "message": "रिपॉजिटरी फिर से लोड हो रही हैं..." }, - "repoLoadFailed": { "message": "रिपॉजिटरी लोड करने में विफल।" }, - "repoTokenPrivate": { "message": "टोकन अमान्य है या निजी रिपॉजिटरी के लिए अधिकार नहीं हैं।" }, - "repoRefetchFailed": { "message": "रिपॉजिटरी फिर से लोड करने में विफल।" }, - "orgSetMessage": { "message": "संगठन सफलतापूर्वक सेट हो गया।" }, - "orgClearedMessage": { "message": "संगठन साफ़ हो गया। सभी गतिविधियाँ प्राप्त करने के लिए 'रिपोर्ट बनाएं' पर क्लिक करें।" }, - "orgNotFoundMessage": { "message": "GitHub पर संगठन नहीं मिला।" }, - "orgValidationErrorMessage": { "message": "संगठन को मान्य करने में त्रुटि।" }, - "refreshingButton": { "message": "रीफ्रेश हो रहा है..." }, - "cacheClearFailed": { "message": "कैश साफ़ करने में विफल।" }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "बनाया जा रहा है..." }, - "copiedButton": { "message": "कॉपी किया गया!" }, - "orgChangedMessage": { "message": "संगठन बदल गया है। GitHub गतिविधियाँ प्राप्त करने के लिए 'रिपोर्ट बनाएं' बटन पर क्लिक करें।" }, - "cacheClearedMessage": { "message": "कैश सफलतापूर्वक साफ़ हो गया। ताज़ा डेटा प्राप्त करने के लिए 'रिपोर्ट बनाएं' पर क्लिक करें।" }, - "cacheClearedButton": { "message": "कैश साफ़ हो गया!" }, - "extensionDisabledMessage": { "message": "एक्सटेंशन अक्षम है। स्क्रम रिपोर्ट बनाने के लिए इसे सेटिंग्स से सक्षम करें।" }, - "notePoint1": { "message": "प्राप्त PRs किसी भी योगदानकर्ता द्वारा सबसे हालिया समीक्षा पर आधारित हैं। यदि आपने 10 दिन पहले एक PR की समीक्षा की और किसी और ने 2 दिन पहले, तो यह पिछले सप्ताह की आपकी गतिविधि में फिर भी दिखाई देगा।" }, - "notePoint2": { "message": "कृपया ध्यान दें कि उत्पन्न स्क्रम में कुछ विसंगतियां हो सकती हैं। हम साझा करने से पहले सटीकता सुनिश्चित करने के लिए रिपोर्ट की मैन्युअल रूप से समीक्षा और संपादन करने की सलाह देते हैं।" }, - "noteSeeIssue": { "message": "यह समस्या देखें" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "एक चयनित अवधि के लिए अपनी Git गतिविधि को स्वतः प्राप्त करके अपनी विकास प्रगति की रिपोर्ट करें।" + }, + "disableLabel": { + "message": "अक्षम करें" + }, + "enableLabel": { + "message": "सक्षम करें" + }, + "homeButtonTitle": { + "message": "रिपोर्ट पर जाएं" + }, + "projectNameLabel": { + "message": "आपकी परियोजना का नाम" + }, + "projectNamePlaceholder": { + "message": "अपनी परियोजना का नाम दर्ज करें" + }, + "githubUsernameLabel": { + "message": "आपका GitHub उपयोगकर्ता नाम" + }, + "githubUsernamePlaceholder": { + "message": "आपके योगदान प्राप्त करने के लिए आवश्यक" + }, + "contributionsLabel": { + "message": "इस अवधि के योगदान प्राप्त करें:" + }, + "last7DaysLabel": { + "message": "पिछले 7 दिन" + }, + "last1DayLabel": { + "message": "कल" + }, + "startDateLabel": { + "message": "प्रारंभ तिथि:" + }, + "endDateLabel": { + "message": "अंतिम तिथि:" + }, + "showOpenClosedLabel": { + "message": "खुला/बंद स्थिति दिखाएं" + }, + "blockersLabel": { + "message": "आपको प्रगति करने से क्या रोक रहा है?" + }, + "blockersPlaceholder": { + "message": "अपना कारण दर्ज करें" + }, + "scrumReportLabel": { + "message": "स्क्रम रिपोर्ट" + }, + "generateReportButton": { + "message": "रिपोर्ट बनाएं" + }, + "copyReportButton": { + "message": "रिपोर्ट कॉपी करें" + }, + "settingsOrgNameLabel": { + "message": "संगठन का नाम" + }, + "settingsOrgNamePlaceholder": { + "message": "संगठन का नाम दर्ज करें" + }, + "setButton": { + "message": "सेट करें" + }, + "githubTokenLabel": { + "message": "आपका GitHub टोकन" + }, + "githubTokenPlaceholder": { + "message": "प्रमाणित अनुरोधों के लिए आवश्यक" + }, + "showCommitsLabel": { + "message": "चयनित अवधि में खुले PRs में किए गए कमिट दिखाएं" + }, + "cacheTTLLabel": { + "message": "कैश TTL दर्ज करें" + }, + "cacheTTLUnit": { + "message": "(मिनटों में)" + }, + "cacheTTLPlaceholder": { + "message": "कैश TTL मिनटों में लिखें (डिफ़ॉल्ट: 10)" + }, + "refreshDataButton": { + "message": "डेटा रीफ्रेश करें (कैश को बायपास करें)" + }, + "refreshDataInfo": { + "message": "ताज़ा डेटा तुरंत प्राप्त करने के लिए इस बटन का उपयोग करें।" + }, + "noteTitle": { + "message": "ध्यान दें:" + }, + "viewCodeButton": { + "message": "कोड देखें" + }, + "reportIssueButton": { + "message": "समस्या की रिपोर्ट करें" + }, + "repoFilterLabel": { + "message": "रिपॉजिटरी द्वारा फ़िल्टर करें" + }, + "enableFilteringLabel": { + "message": "फ़िल्टरिंग सक्षम करें" + }, + "tokenRequiredWarning": { + "message": "रिपॉजिटरी फ़िल्टरिंग के लिए एक GitHub टोकन आवश्यक है। कृपया सेटिंग्स में एक जोड़ें।" + }, + "repoSearchPlaceholder": { + "message": "जोड़ने के लिए एक रिपॉजिटरी खोजें..." + }, + "repoPlaceholder": { + "message": "कोई रिपॉजिटरी चयनित नहीं (सभी शामिल होंगी)" + }, + "repoCountNone": { + "message": "0 रिपॉजिटरी चयनित" + }, + "repoCount": { + "message": "$1 रिपॉजिटरी चयनित" + }, + "repoLoading": { + "message": "रिपॉजिटरी लोड हो रही हैं..." + }, + "repoLoaded": { + "message": "$1 रिपॉजिटरी लोड हो गईं।" + }, + "repoNotFound": { + "message": "कोई रिपॉजिटरी नहीं मिली" + }, + "repoRefetching": { + "message": "रिपॉजिटरी फिर से लोड हो रही हैं..." + }, + "repoLoadFailed": { + "message": "रिपॉजिटरी लोड करने में विफल।" + }, + "repoTokenPrivate": { + "message": "टोकन अमान्य है या निजी रिपॉजिटरी के लिए अधिकार नहीं हैं।" + }, + "repoRefetchFailed": { + "message": "रिपॉजिटरी फिर से लोड करने में विफल।" + }, + "orgSetMessage": { + "message": "संगठन सफलतापूर्वक सेट हो गया।" + }, + "orgClearedMessage": { + "message": "संगठन साफ़ हो गया। सभी गतिविधियाँ प्राप्त करने के लिए 'रिपोर्ट बनाएं' पर क्लिक करें।" + }, + "orgNotFoundMessage": { + "message": "GitHub पर संगठन नहीं मिला।" + }, + "orgValidationErrorMessage": { + "message": "संगठन को मान्य करने में त्रुटि।" + }, + "refreshingButton": { + "message": "रीफ्रेश हो रहा है..." + }, + "cacheClearFailed": { + "message": "कैश साफ़ करने में विफल।" + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "बनाया जा रहा है..." + }, + "copiedButton": { + "message": "कॉपी किया गया!" + }, + "orgChangedMessage": { + "message": "संगठन बदल गया है। GitHub गतिविधियाँ प्राप्त करने के लिए 'रिपोर्ट बनाएं' बटन पर क्लिक करें।" + }, + "cacheClearedMessage": { + "message": "कैश सफलतापूर्वक साफ़ हो गया। ताज़ा डेटा प्राप्त करने के लिए 'रिपोर्ट बनाएं' पर क्लिक करें।" + }, + "cacheClearedButton": { + "message": "कैश साफ़ हो गया!" + }, + "extensionDisabledMessage": { + "message": "एक्सटेंशन अक्षम है। स्क्रम रिपोर्ट बनाने के लिए इसे सेटिंग्स से सक्षम करें।" + }, + "notePoint1": { + "message": "प्राप्त PRs किसी भी योगदानकर्ता द्वारा सबसे हालिया समीक्षा पर आधारित हैं। यदि आपने 10 दिन पहले एक PR की समीक्षा की और किसी और ने 2 दिन पहले, तो यह पिछले सप्ताह की आपकी गतिविधि में फिर भी दिखाई देगा।" + }, + "notePoint2": { + "message": "कृपया ध्यान दें कि उत्पन्न स्क्रम में कुछ विसंगतियां हो सकती हैं। हम साझा करने से पहले सटीकता सुनिश्चित करने के लिए रिपोर्ट की मैन्युअल रूप से समीक्षा और संपादन करने की सलाह देते हैं।" + }, + "noteSeeIssue": { + "message": "यह समस्या देखें" + }, + "platformLabel": { + "message": "प्लेटफ़ॉर्म" + }, + "usernameLabel": { + "message": "आपका उपयोगकर्ता नाम" + } } \ No newline at end of file diff --git a/src/_locales/id/messages.json b/src/_locales/id/messages.json index e9f844b..ec75f9a 100644 --- a/src/_locales/id/messages.json +++ b/src/_locales/id/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Laporkan kemajuan pengembangan Anda dengan mengambil aktivitas Git Anda secara otomatis untuk periode yang dipilih." }, - "disableLabel": { "message": "Nonaktifkan" }, - "enableLabel": { "message": "Aktifkan" }, - "homeButtonTitle": { "message": "Buka Laporan" }, - "projectNameLabel": { "message": "Nama Proyek Anda" }, - "projectNamePlaceholder": { "message": "Masukkan nama proyek Anda" }, - "githubUsernameLabel": { "message": "Nama Pengguna GitHub Anda" }, - "githubUsernamePlaceholder": { "message": "Diperlukan untuk mengambil kontribusi Anda" }, - "contributionsLabel": { "message": "Ambil kontribusi Anda untuk:" }, - "last7DaysLabel": { "message": "7 hari terakhir" }, - "last1DayLabel": { "message": "Kemarin" }, - "startDateLabel": { "message": "Tanggal Mulai:" }, - "endDateLabel": { "message": "Tanggal Selesai:" }, - "showOpenClosedLabel": { "message": "Tampilkan status Buka/Tutup" }, - "blockersLabel": { "message": "Apa yang menghalangi kemajuan Anda?" }, - "blockersPlaceholder": { "message": "Masukkan alasan Anda" }, - "scrumReportLabel": { "message": "Laporan Scrum" }, - "generateReportButton": { "message": "Buat Laporan" }, - "copyReportButton": { "message": "Salin Laporan" }, - "settingsOrgNameLabel": { "message": "Nama Organisasi" }, - "settingsOrgNamePlaceholder": { "message": "Masukkan nama organisasi" }, - "setButton": { "message": "Atur" }, - "githubTokenLabel": { "message": "Token GitHub Anda" }, - "githubTokenPlaceholder": { "message": "Diperlukan untuk permintaan terotentikasi" }, - "showCommitsLabel": { "message": "Tampilkan commit di PR terbuka dalam rentang tanggal" }, - "cacheTTLLabel": { "message": "Masukkan TTL cache" }, - "cacheTTLUnit": { "message": "(dalam menit)" }, - "cacheTTLPlaceholder": { "message": "Tulis TTL Cache dalam menit (Default: 10)" }, - "refreshDataButton": { "message": "Segarkan Data (abaikan cache)" }, - "refreshDataInfo": { "message": "Gunakan tombol ini untuk mengambil data baru segera." }, - "noteTitle": { "message": "Catatan:" }, - "viewCodeButton": { "message": "Lihat Kode" }, - "reportIssueButton": { "message": "Laporkan Masalah" }, - "repoFilterLabel": { "message": "Filter berdasarkan repositori" }, - "enableFilteringLabel": { "message": "Aktifkan pemfilteran" }, - "tokenRequiredWarning": { "message": "Token GitHub diperlukan untuk pemfilteran repositori. Harap tambahkan di pengaturan." }, - "repoSearchPlaceholder": { "message": "Cari repositori untuk ditambahkan..." }, - "repoPlaceholder": { "message": "Tidak ada repositori yang dipilih (semua akan disertakan)" }, - "repoCountNone": { "message": "0 repositori dipilih" }, - "repoCount": { "message": "$1 repositori dipilih" }, - "repoLoading": { "message": "Memuat repositori..." }, - "repoLoaded": { "message": "$1 repositori dimuat." }, - "repoNotFound": { "message": "Tidak ada repositori ditemukan" }, - "repoRefetching": { "message": "Memuat ulang repositori..." }, - "repoLoadFailed": { "message": "Gagal memuat repositori." }, - "repoTokenPrivate": { "message": "Token tidak valid atau tidak memiliki hak untuk repo pribadi." }, - "repoRefetchFailed": { "message": "Gagal memuat ulang repositori." }, - "orgSetMessage": { "message": "Organisasi berhasil diatur." }, - "orgClearedMessage": { "message": "Organisasi dihapus. Klik 'Buat Laporan' untuk mengambil semua aktivitas." }, - "orgNotFoundMessage": { "message": "Organisasi tidak ditemukan di GitHub." }, - "orgValidationErrorMessage": { "message": "Gagal memvalidasi organisasi." }, - "refreshingButton": { "message": "Menyegarkan..." }, - "cacheClearFailed": { "message": "Gagal membersihkan cache." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Membuat..." }, - "copiedButton": { "message": "Tersalin!" }, - "orgChangedMessage": { "message": "Organisasi diubah. Klik tombol 'Buat Laporan' untuk mengambil aktivitas GitHub." }, - "cacheClearedMessage": { "message": "Cache berhasil dihapus. Klik 'Buat Laporan' untuk mengambil data baru." }, - "cacheClearedButton": { "message": "Cache Dihapus!" }, - "extensionDisabledMessage": { "message": "Ekstensi dinonaktifkan. Aktifkan dari pengaturan untuk membuat laporan scrum." }, - "notePoint1": { "message": "PR yang diambil didasarkan pada ulasan terbaru oleh kontributor mana pun. Jika Anda meninjau PR 10 hari yang lalu dan orang lain meninjaunya 2 hari yang lalu, itu akan tetap muncul di aktivitas Anda selama seminggu terakhir." }, - "notePoint2": { "message": "Harap dicatat bahwa beberapa perbedaan mungkin terjadi pada Scrum yang dihasilkan. Kami menyarankan untuk meninjau dan mengedit laporan secara manual untuk memastikan akurasi sebelum membagikannya." }, - "noteSeeIssue": { "message": "Lihat masalah ini" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Laporkan kemajuan pengembangan Anda dengan mengambil aktivitas Git Anda secara otomatis untuk periode yang dipilih." + }, + "disableLabel": { + "message": "Nonaktifkan" + }, + "enableLabel": { + "message": "Aktifkan" + }, + "homeButtonTitle": { + "message": "Buka Laporan" + }, + "projectNameLabel": { + "message": "Nama Proyek Anda" + }, + "projectNamePlaceholder": { + "message": "Masukkan nama proyek Anda" + }, + "githubUsernameLabel": { + "message": "Nama Pengguna GitHub Anda" + }, + "githubUsernamePlaceholder": { + "message": "Diperlukan untuk mengambil kontribusi Anda" + }, + "contributionsLabel": { + "message": "Ambil kontribusi Anda untuk:" + }, + "last7DaysLabel": { + "message": "7 hari terakhir" + }, + "last1DayLabel": { + "message": "Kemarin" + }, + "startDateLabel": { + "message": "Tanggal Mulai:" + }, + "endDateLabel": { + "message": "Tanggal Selesai:" + }, + "showOpenClosedLabel": { + "message": "Tampilkan status Buka/Tutup" + }, + "blockersLabel": { + "message": "Apa yang menghalangi kemajuan Anda?" + }, + "blockersPlaceholder": { + "message": "Masukkan alasan Anda" + }, + "scrumReportLabel": { + "message": "Laporan Scrum" + }, + "generateReportButton": { + "message": "Buat Laporan" + }, + "copyReportButton": { + "message": "Salin Laporan" + }, + "settingsOrgNameLabel": { + "message": "Nama Organisasi" + }, + "settingsOrgNamePlaceholder": { + "message": "Masukkan nama organisasi" + }, + "setButton": { + "message": "Atur" + }, + "githubTokenLabel": { + "message": "Token GitHub Anda" + }, + "githubTokenPlaceholder": { + "message": "Diperlukan untuk permintaan terotentikasi" + }, + "showCommitsLabel": { + "message": "Tampilkan commit di PR terbuka dalam rentang tanggal" + }, + "cacheTTLLabel": { + "message": "Masukkan TTL cache" + }, + "cacheTTLUnit": { + "message": "(dalam menit)" + }, + "cacheTTLPlaceholder": { + "message": "Tulis TTL Cache dalam menit (Default: 10)" + }, + "refreshDataButton": { + "message": "Segarkan Data (abaikan cache)" + }, + "refreshDataInfo": { + "message": "Gunakan tombol ini untuk mengambil data baru segera." + }, + "noteTitle": { + "message": "Catatan:" + }, + "viewCodeButton": { + "message": "Lihat Kode" + }, + "reportIssueButton": { + "message": "Laporkan Masalah" + }, + "repoFilterLabel": { + "message": "Filter berdasarkan repositori" + }, + "enableFilteringLabel": { + "message": "Aktifkan pemfilteran" + }, + "tokenRequiredWarning": { + "message": "Token GitHub diperlukan untuk pemfilteran repositori. Harap tambahkan di pengaturan." + }, + "repoSearchPlaceholder": { + "message": "Cari repositori untuk ditambahkan..." + }, + "repoPlaceholder": { + "message": "Tidak ada repositori yang dipilih (semua akan disertakan)" + }, + "repoCountNone": { + "message": "0 repositori dipilih" + }, + "repoCount": { + "message": "$1 repositori dipilih" + }, + "repoLoading": { + "message": "Memuat repositori..." + }, + "repoLoaded": { + "message": "$1 repositori dimuat." + }, + "repoNotFound": { + "message": "Tidak ada repositori ditemukan" + }, + "repoRefetching": { + "message": "Memuat ulang repositori..." + }, + "repoLoadFailed": { + "message": "Gagal memuat repositori." + }, + "repoTokenPrivate": { + "message": "Token tidak valid atau tidak memiliki hak untuk repo pribadi." + }, + "repoRefetchFailed": { + "message": "Gagal memuat ulang repositori." + }, + "orgSetMessage": { + "message": "Organisasi berhasil diatur." + }, + "orgClearedMessage": { + "message": "Organisasi dihapus. Klik 'Buat Laporan' untuk mengambil semua aktivitas." + }, + "orgNotFoundMessage": { + "message": "Organisasi tidak ditemukan di GitHub." + }, + "orgValidationErrorMessage": { + "message": "Gagal memvalidasi organisasi." + }, + "refreshingButton": { + "message": "Menyegarkan..." + }, + "cacheClearFailed": { + "message": "Gagal membersihkan cache." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Membuat..." + }, + "copiedButton": { + "message": "Tersalin!" + }, + "orgChangedMessage": { + "message": "Organisasi diubah. Klik tombol 'Buat Laporan' untuk mengambil aktivitas GitHub." + }, + "cacheClearedMessage": { + "message": "Cache berhasil dihapus. Klik 'Buat Laporan' untuk mengambil data baru." + }, + "cacheClearedButton": { + "message": "Cache Dihapus!" + }, + "extensionDisabledMessage": { + "message": "Ekstensi dinonaktifkan. Aktifkan dari pengaturan untuk membuat laporan scrum." + }, + "notePoint1": { + "message": "PR yang diambil didasarkan pada ulasan terbaru oleh kontributor mana pun. Jika Anda meninjau PR 10 hari yang lalu dan orang lain meninjaunya 2 hari yang lalu, itu akan tetap muncul di aktivitas Anda selama seminggu terakhir." + }, + "notePoint2": { + "message": "Harap dicatat bahwa beberapa perbedaan mungkin terjadi pada Scrum yang dihasilkan. Kami menyarankan untuk meninjau dan mengedit laporan secara manual untuk memastikan akurasi sebelum membagikannya." + }, + "noteSeeIssue": { + "message": "Lihat masalah ini" + }, + "platformLabel": { + "message": "Platform" + }, + "usernameLabel": { + "message": "Nama pengguna Anda" + } } \ No newline at end of file diff --git a/src/_locales/ja/messages.json b/src/_locales/ja/messages.json index 9f9b5cd..00394eb 100644 --- a/src/_locales/ja/messages.json +++ b/src/_locales/ja/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "選択した期間のGitアクティビティを自動的に取得して、開発の進捗を報告します。" }, - "disableLabel": { "message": "無効にする" }, - "enableLabel": { "message": "有効にする" }, - "homeButtonTitle": { "message": "レポートに移動" }, - "projectNameLabel": { "message": "プロジェクト名" }, - "projectNamePlaceholder": { "message": "プロジェクト名を入力してください" }, - "githubUsernameLabel": { "message": "GitHubユーザー名" }, - "githubUsernamePlaceholder": { "message": "コントリビューションの取得に必要です" }, - "contributionsLabel": { "message": "取得するコントリビューションの期間:" }, - "last7DaysLabel": { "message": "過去7日間" }, - "last1DayLabel": { "message": "昨日" }, - "startDateLabel": { "message": "開始日:" }, - "endDateLabel": { "message": "終了日:" }, - "showOpenClosedLabel": { "message": "オープン/クローズ状態を表示" }, - "blockersLabel": { "message": "進捗の妨げになっているものは何ですか?" }, - "blockersPlaceholder": { "message": "理由を入力してください" }, - "scrumReportLabel": { "message": "スクラムレポート" }, - "generateReportButton": { "message": "レポートを生成" }, - "copyReportButton": { "message": "レポートをコピー" }, - "settingsOrgNameLabel": { "message": "組織名" }, - "settingsOrgNamePlaceholder": { "message": "組織名を入力してください" }, - "setButton": { "message": "設定" }, - "githubTokenLabel": { "message": "GitHubトークン" }, - "githubTokenPlaceholder": { "message": "認証済みリクエストに必要です" }, - "showCommitsLabel": { "message": "期間内のオープンなPRのコミットを表示" }, - "cacheTTLLabel": { "message": "キャッシュTTLを入力" }, - "cacheTTLUnit": { "message": "(分)" }, - "cacheTTLPlaceholder": { "message": "キャッシュTTLを分単位で入力 (デフォルト: 10)" }, - "refreshDataButton": { "message": "データを更新 (キャッシュを無視)" }, - "refreshDataInfo": { "message": "このボタンで最新のデータをすぐに取得します。" }, - "noteTitle": { "message": "注:" }, - "viewCodeButton": { "message": "コードを見る" }, - "reportIssueButton": { "message": "問題を報告" }, - "repoFilterLabel": { "message": "リポジトリでフィルタリング" }, - "enableFilteringLabel": { "message": "フィルタリングを有効にする" }, - "tokenRequiredWarning": { "message": "リポジトリのフィルタリングにはGitHubトークンが必要です。設定で追加してください。" }, - "repoSearchPlaceholder": { "message": "追加するリポジトリを検索..." }, - "repoPlaceholder": { "message": "リポジトリが選択されていません(すべて含まれます)" }, - "repoCountNone": { "message": "0件のリポジトリが選択されています" }, - "repoCount": { "message": "$1件のリポジトリが選択されています" }, - "repoLoading": { "message": "リポジトリを読み込み中..." }, - "repoLoaded": { "message": "$1件のリポジトリを読み込みました。" }, - "repoNotFound": { "message": "リポジトリが見つかりません" }, - "repoRefetching": { "message": "リポジトリを再読み込み中..." }, - "repoLoadFailed": { "message": "リポジトリの読み込みに失敗しました。" }, - "repoTokenPrivate": { "message": "トークンが無効か、プライベートリポジトリへの権限がありません。" }, - "repoRefetchFailed": { "message": "リポジトリの再読み込みに失敗しました。" }, - "orgSetMessage": { "message": "組織が正常に設定されました。" }, - "orgClearedMessage": { "message": "組織がクリアされました。「レポートを生成」をクリックしてすべてのアクティビティを取得してください。" }, - "orgNotFoundMessage": { "message": "GitHubで組織が見つかりません。" }, - "orgValidationErrorMessage": { "message": "組織の検証中にエラーが発生しました。" }, - "refreshingButton": { "message": "更新中..." }, - "cacheClearFailed": { "message": "キャッシュのクリアに失敗しました。" }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "生成中..." }, - "copiedButton": { "message": "コピーしました!" }, - "orgChangedMessage": { "message": "組織が変更されました。「レポートを生成」ボタンをクリックしてGitHubアクティビティを取得してください。" }, - "cacheClearedMessage": { "message": "キャッシュが正常にクリアされました。「レポートを生成」をクリックして新しいデータを取得してください。" }, - "cacheClearedButton": { "message": "キャッシュクリア!" }, - "extensionDisabledMessage": { "message": "拡張機能が無効です。スクラムレポートを生成するには、設定から有効にしてください。" }, - "notePoint1": { "message": "取得されるPRは、いずれかの貢献者による最新のレビューに基づきます。10日前にPRをレビューし、他の誰かが2日前にレビューした場合でも、過去1週間のアクティビティに表示されます。" }, - "notePoint2": { "message": "生成されたスクラムには不一致が生じる可能性があります。共有する前に、レポートを手動で確認・編集して正確性を確保することをお勧めします。" }, - "noteSeeIssue": { "message": "このIssueを参照" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "選択した期間のGitアクティビティを自動的に取得して、開発の進捗を報告します。" + }, + "disableLabel": { + "message": "無効にする" + }, + "enableLabel": { + "message": "有効にする" + }, + "homeButtonTitle": { + "message": "レポートに移動" + }, + "projectNameLabel": { + "message": "プロジェクト名" + }, + "projectNamePlaceholder": { + "message": "プロジェクト名を入力してください" + }, + "githubUsernameLabel": { + "message": "GitHubユーザー名" + }, + "githubUsernamePlaceholder": { + "message": "コントリビューションの取得に必要です" + }, + "contributionsLabel": { + "message": "取得するコントリビューションの期間:" + }, + "last7DaysLabel": { + "message": "過去7日間" + }, + "last1DayLabel": { + "message": "昨日" + }, + "startDateLabel": { + "message": "開始日:" + }, + "endDateLabel": { + "message": "終了日:" + }, + "showOpenClosedLabel": { + "message": "オープン/クローズ状態を表示" + }, + "blockersLabel": { + "message": "進捗の妨げになっているものは何ですか?" + }, + "blockersPlaceholder": { + "message": "理由を入力してください" + }, + "scrumReportLabel": { + "message": "スクラムレポート" + }, + "generateReportButton": { + "message": "レポートを生成" + }, + "copyReportButton": { + "message": "レポートをコピー" + }, + "settingsOrgNameLabel": { + "message": "組織名" + }, + "settingsOrgNamePlaceholder": { + "message": "組織名を入力してください" + }, + "setButton": { + "message": "設定" + }, + "githubTokenLabel": { + "message": "GitHubトークン" + }, + "githubTokenPlaceholder": { + "message": "認証済みリクエストに必要です" + }, + "showCommitsLabel": { + "message": "期間内のオープンなPRのコミットを表示" + }, + "cacheTTLLabel": { + "message": "キャッシュTTLを入力" + }, + "cacheTTLUnit": { + "message": "(分)" + }, + "cacheTTLPlaceholder": { + "message": "キャッシュTTLを分単位で入力 (デフォルト: 10)" + }, + "refreshDataButton": { + "message": "データを更新 (キャッシュを無視)" + }, + "refreshDataInfo": { + "message": "このボタンで最新のデータをすぐに取得します。" + }, + "noteTitle": { + "message": "注:" + }, + "viewCodeButton": { + "message": "コードを見る" + }, + "reportIssueButton": { + "message": "問題を報告" + }, + "repoFilterLabel": { + "message": "リポジトリでフィルタリング" + }, + "enableFilteringLabel": { + "message": "フィルタリングを有効にする" + }, + "tokenRequiredWarning": { + "message": "リポジトリのフィルタリングにはGitHubトークンが必要です。設定で追加してください。" + }, + "repoSearchPlaceholder": { + "message": "追加するリポジトリを検索..." + }, + "repoPlaceholder": { + "message": "リポジトリが選択されていません(すべて含まれます)" + }, + "repoCountNone": { + "message": "0件のリポジトリが選択されています" + }, + "repoCount": { + "message": "$1件のリポジトリが選択されています" + }, + "repoLoading": { + "message": "リポジトリを読み込み中..." + }, + "repoLoaded": { + "message": "$1件のリポジトリを読み込みました。" + }, + "repoNotFound": { + "message": "リポジトリが見つかりません" + }, + "repoRefetching": { + "message": "リポジトリを再読み込み中..." + }, + "repoLoadFailed": { + "message": "リポジトリの読み込みに失敗しました。" + }, + "repoTokenPrivate": { + "message": "トークンが無効か、プライベートリポジトリへの権限がありません。" + }, + "repoRefetchFailed": { + "message": "リポジトリの再読み込みに失敗しました。" + }, + "orgSetMessage": { + "message": "組織が正常に設定されました。" + }, + "orgClearedMessage": { + "message": "組織がクリアされました。「レポートを生成」をクリックしてすべてのアクティビティを取得してください。" + }, + "orgNotFoundMessage": { + "message": "GitHubで組織が見つかりません。" + }, + "orgValidationErrorMessage": { + "message": "組織の検証中にエラーが発生しました。" + }, + "refreshingButton": { + "message": "更新中..." + }, + "cacheClearFailed": { + "message": "キャッシュのクリアに失敗しました。" + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "生成中..." + }, + "copiedButton": { + "message": "コピーしました!" + }, + "orgChangedMessage": { + "message": "組織が変更されました。「レポートを生成」ボタンをクリックしてGitHubアクティビティを取得してください。" + }, + "cacheClearedMessage": { + "message": "キャッシュが正常にクリアされました。「レポートを生成」をクリックして新しいデータを取得してください。" + }, + "cacheClearedButton": { + "message": "キャッシュクリア!" + }, + "extensionDisabledMessage": { + "message": "拡張機能が無効です。スクラムレポートを生成するには、設定から有効にしてください。" + }, + "notePoint1": { + "message": "取得されるPRは、いずれかの貢献者による最新のレビューに基づきます。10日前にPRをレビューし、他の誰かが2日前にレビューした場合でも、過去1週間のアクティビティに表示されます。" + }, + "notePoint2": { + "message": "生成されたスクラムには不一致が生じる可能性があります。共有する前に、レポートを手動で確認・編集して正確性を確保することをお勧めします。" + }, + "noteSeeIssue": { + "message": "このIssueを参照" + }, + "platformLabel": { + "message": "プラットフォーム" + }, + "usernameLabel": { + "message": "あなたのユーザー名" + } } \ No newline at end of file diff --git a/src/_locales/pt/messages.json b/src/_locales/pt/messages.json index 77eb9e4..dc7d4b3 100644 --- a/src/_locales/pt/messages.json +++ b/src/_locales/pt/messages.json @@ -1,46 +1,140 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Relate o seu progresso de desenvolvimento buscando automaticamente a sua atividade no Git para um período selecionado" }, - "disableLabel": { "message": "Desativar" }, - "enableLabel": { "message": "Ativar" }, - "homeButtonTitle": { "message": "Ir para o Relatório" }, - "projectNameLabel": { "message": "Nome do seu Projeto" }, - "projectNamePlaceholder": { "message": "Digite o nome do seu projeto" }, - "githubUsernameLabel": { "message": "Seu Nome de Usuário do Github" }, - "githubUsernamePlaceholder": { "message": "Necessário para buscar suas contribuições" }, - "contributionsLabel": { "message": "Buscar suas contribuições entre:" }, - "last7DaysLabel": { "message": "Últimos 7 dias" }, - "last1DayLabel": { "message": "Último dia" }, - "startDateLabel": { "message": "Data de Início:" }, - "endDateLabel": { "message": "Data de Fim:" }, - "showOpenClosedLabel": { "message": "Mostrar Rótulo Aberto/Fechado" }, - "blockersLabel": { "message": "O que está impedindo você de progredir?" }, - "blockersPlaceholder": { "message": "Digite o seu motivo" }, - "scrumReportLabel": { "message": "Relatório Scrum" }, - "generateReportButton": { "message": "Gerar Relatório" }, - "copyReportButton": { "message": "Copiar Relatório" }, - "settingsOrgNameLabel": { "message": "Nome da Organização" }, - "settingsOrgNamePlaceholder": { "message": "Digite o nome da organização" }, - "setButton": { "message": "Definir" }, - "githubTokenLabel": { "message": "Seu Token do Github" }, - "githubTokenPlaceholder": { "message": "Necessário para fazer solicitações autenticadas" }, - "showCommitsLabel": { "message": "Mostrar commits feitos dentro do intervalo de datas em PRs abertos" }, - "cacheTTLLabel": { "message": "Digite o TTL do cache" }, - "cacheTTLUnit": { "message": "(em minutos)" }, - "cacheTTLPlaceholder": { "message": "Escreva o TTL do cache em minutos (Padrão 10 minutos)" }, - "refreshDataButton": { "message": "Atualizar Dados (ignorar cache)" }, - "refreshDataInfo": { "message": "Use este botão para buscar dados novos imediatamente." }, - "noteTitle": { "message": "Nota:" }, - "viewCodeButton": { "message": "Ver Código" }, - "reportIssueButton": { "message": "Relatar Problema" }, - "madeWithLoveBy": { "message": "Feito com ❤️ por" }, - "generatingButton": { "message": "Gerando..." }, - "copiedButton": { "message": "Copiado!" }, - "orgChangedMessage": { "message": "Organização alterada. Clique no botão Gerar para buscar as atividades do GitHub." }, - "cacheClearedMessage": { "message": "Cache limpo com sucesso. Clique em \"Gerar Relatório\" para buscar dados novos." }, - "cacheClearedButton": { "message": "Cache Limpo!" }, - "extensionDisabledMessage": { "message": "A extensão está desativada. Ative-a nas opções para gerar relatórios scrum." }, - "notePoint1": { "message": "Os PRs buscados são baseados na revisão mais recente de qualquer contribuidor. Se você revisou um PR há 10 dias e outra pessoa o revisou há 2 dias, ele ainda aparecerá na sua atividade da última semana." }, - "notePoint2": { "message": "Observe que algumas discrepâncias podem ocorrer no SCRUM gerado. Recomendamos revisar e editar manualmente o relatório para garantir a precisão antes de compartilhar." }, - "noteSeeIssue": { "message": "Veja este problema" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Relate o seu progresso de desenvolvimento buscando automaticamente a sua atividade no Git para um período selecionado" + }, + "disableLabel": { + "message": "Desativar" + }, + "enableLabel": { + "message": "Ativar" + }, + "homeButtonTitle": { + "message": "Ir para o Relatório" + }, + "projectNameLabel": { + "message": "Nome do seu Projeto" + }, + "projectNamePlaceholder": { + "message": "Digite o nome do seu projeto" + }, + "githubUsernameLabel": { + "message": "Seu Nome de Usuário do Github" + }, + "githubUsernamePlaceholder": { + "message": "Necessário para buscar suas contribuições" + }, + "contributionsLabel": { + "message": "Buscar suas contribuições entre:" + }, + "last7DaysLabel": { + "message": "Últimos 7 dias" + }, + "last1DayLabel": { + "message": "Último dia" + }, + "startDateLabel": { + "message": "Data de Início:" + }, + "endDateLabel": { + "message": "Data de Fim:" + }, + "showOpenClosedLabel": { + "message": "Mostrar Rótulo Aberto/Fechado" + }, + "blockersLabel": { + "message": "O que está impedindo você de progredir?" + }, + "blockersPlaceholder": { + "message": "Digite o seu motivo" + }, + "scrumReportLabel": { + "message": "Relatório Scrum" + }, + "generateReportButton": { + "message": "Gerar Relatório" + }, + "copyReportButton": { + "message": "Copiar Relatório" + }, + "settingsOrgNameLabel": { + "message": "Nome da Organização" + }, + "settingsOrgNamePlaceholder": { + "message": "Digite o nome da organização" + }, + "setButton": { + "message": "Definir" + }, + "githubTokenLabel": { + "message": "Seu Token do Github" + }, + "githubTokenPlaceholder": { + "message": "Necessário para fazer solicitações autenticadas" + }, + "showCommitsLabel": { + "message": "Mostrar commits feitos dentro do intervalo de datas em PRs abertos" + }, + "cacheTTLLabel": { + "message": "Digite o TTL do cache" + }, + "cacheTTLUnit": { + "message": "(em minutos)" + }, + "cacheTTLPlaceholder": { + "message": "Escreva o TTL do cache em minutos (Padrão 10 minutos)" + }, + "refreshDataButton": { + "message": "Atualizar Dados (ignorar cache)" + }, + "refreshDataInfo": { + "message": "Use este botão para buscar dados novos imediatamente." + }, + "noteTitle": { + "message": "Nota:" + }, + "viewCodeButton": { + "message": "Ver Código" + }, + "reportIssueButton": { + "message": "Relatar Problema" + }, + "madeWithLoveBy": { + "message": "Feito com ❤️ por" + }, + "generatingButton": { + "message": "Gerando..." + }, + "copiedButton": { + "message": "Copiado!" + }, + "orgChangedMessage": { + "message": "Organização alterada. Clique no botão Gerar para buscar as atividades do GitHub." + }, + "cacheClearedMessage": { + "message": "Cache limpo com sucesso. Clique em \"Gerar Relatório\" para buscar dados novos." + }, + "cacheClearedButton": { + "message": "Cache Limpo!" + }, + "extensionDisabledMessage": { + "message": "A extensão está desativada. Ative-a nas opções para gerar relatórios scrum." + }, + "notePoint1": { + "message": "Os PRs buscados são baseados na revisão mais recente de qualquer contribuidor. Se você revisou um PR há 10 dias e outra pessoa o revisou há 2 dias, ele ainda aparecerá na sua atividade da última semana." + }, + "notePoint2": { + "message": "Observe que algumas discrepâncias podem ocorrer no SCRUM gerado. Recomendamos revisar e editar manualmente o relatório para garantir a precisão antes de compartilhar." + }, + "noteSeeIssue": { + "message": "Veja este problema" + }, + "platformLabel": { + "message": "Plataforma" + }, + "usernameLabel": { + "message": "Seu nome de usuário" + } } \ No newline at end of file diff --git a/src/_locales/pt_BR/messages.json b/src/_locales/pt_BR/messages.json index dc2d634..67a20f6 100644 --- a/src/_locales/pt_BR/messages.json +++ b/src/_locales/pt_BR/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Relate seu progresso de desenvolvimento buscando automaticamente sua atividade no Git para um período selecionado." }, - "disableLabel": { "message": "Desativar" }, - "enableLabel": { "message": "Ativar" }, - "homeButtonTitle": { "message": "Ir para o Relatório" }, - "projectNameLabel": { "message": "Nome do seu Projeto" }, - "projectNamePlaceholder": { "message": "Digite o nome do seu projeto" }, - "githubUsernameLabel": { "message": "Seu nome de usuário do GitHub" }, - "githubUsernamePlaceholder": { "message": "Necessário para buscar suas contribuições" }, - "contributionsLabel": { "message": "Buscar suas contribuições de:" }, - "last7DaysLabel": { "message": "Últimos 7 dias" }, - "last1DayLabel": { "message": "Ontem" }, - "startDateLabel": { "message": "Data de Início:" }, - "endDateLabel": { "message": "Data de Fim:" }, - "showOpenClosedLabel": { "message": "Mostrar status Aberto/Fechado" }, - "blockersLabel": { "message": "O que está impedindo seu progresso?" }, - "blockersPlaceholder": { "message": "Digite o motivo" }, - "scrumReportLabel": { "message": "Relatório Scrum" }, - "generateReportButton": { "message": "Gerar Relatório" }, - "copyReportButton": { "message": "Copiar Relatório" }, - "settingsOrgNameLabel": { "message": "Nome da Organização" }, - "settingsOrgNamePlaceholder": { "message": "Digite o nome da organização" }, - "setButton": { "message": "Definir" }, - "githubTokenLabel": { "message": "Seu Token do GitHub" }, - "githubTokenPlaceholder": { "message": "Necessário para solicitações autenticadas" }, - "showCommitsLabel": { "message": "Mostrar commits em PRs abertos no período" }, - "cacheTTLLabel": { "message": "Digite o TTL do cache" }, - "cacheTTLUnit": { "message": "(em minutos)" }, - "cacheTTLPlaceholder": { "message": "Digite o TTL do cache em minutos (Padrão: 10)" }, - "refreshDataButton": { "message": "Atualizar Dados (ignorar cache)" }, - "refreshDataInfo": { "message": "Use este botão para buscar dados novos imediatamente." }, - "noteTitle": { "message": "Nota:" }, - "viewCodeButton": { "message": "Ver código" }, - "reportIssueButton": { "message": "Relatar um problema" }, - "repoFilterLabel": { "message": "Filtrar por repositórios" }, - "enableFilteringLabel": { "message": "Ativar filtragem" }, - "tokenRequiredWarning": { "message": "Um token do GitHub é necessário para filtrar repositórios. Adicione um nas configurações." }, - "repoSearchPlaceholder": { "message": "Buscar um repositório para adicionar..." }, - "repoPlaceholder": { "message": "Nenhum repositório selecionado (todos serão incluídos)" }, - "repoCountNone": { "message": "0 repositórios selecionados" }, - "repoCount": { "message": "$1 repositórios selecionados" }, - "repoLoading": { "message": "Carregando repositórios..." }, - "repoLoaded": { "message": "$1 repositórios carregados." }, - "repoNotFound": { "message": "Nenhum repositório encontrado" }, - "repoRefetching": { "message": "Recarregando repositórios..." }, - "repoLoadFailed": { "message": "Falha ao carregar repositórios." }, - "repoTokenPrivate": { "message": "O token é inválido ou não tem permissão para repositórios privados." }, - "repoRefetchFailed": { "message": "Falha ao recarregar repositórios." }, - "orgSetMessage": { "message": "Organização definida com sucesso." }, - "orgClearedMessage": { "message": "Organização removida. Clique em 'Gerar Relatório' para buscar todas as atividades." }, - "orgNotFoundMessage": { "message": "Organização não encontrada no GitHub." }, - "orgValidationErrorMessage": { "message": "Erro ao validar a organização." }, - "refreshingButton": { "message": "Atualizando..." }, - "cacheClearFailed": { "message": "Falha ao limpar o cache." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Gerando..." }, - "copiedButton": { "message": "Copiado!" }, - "orgChangedMessage": { "message": "Organização alterada. Clique em 'Gerar Relatório' para buscar as atividades do GitHub." }, - "cacheClearedMessage": { "message": "Cache limpo com sucesso. Clique em 'Gerar Relatório' para buscar dados novos." }, - "cacheClearedButton": { "message": "Cache Limpo!" }, - "extensionDisabledMessage": { "message": "A extensão está desativada. Ative-a nos ajustes para gerar relatórios scrum." }, - "notePoint1": { "message": "Os PRs buscados são baseados na revisão mais recente de qualquer contribuidor. Se você revisou um PR há 10 dias e outra pessoa o revisou há 2 dias, ele ainda aparecerá na sua atividade da última semana." }, - "notePoint2": { "message": "Observe que algumas discrepâncias podem ocorrer no Scrum gerado. Recomendamos revisar e editar manualmente o relatório para garantir a precisão antes de compartilhar." }, - "noteSeeIssue": { "message": "Veja este issue" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Relate seu progresso de desenvolvimento buscando automaticamente sua atividade no Git para um período selecionado." + }, + "disableLabel": { + "message": "Desativar" + }, + "enableLabel": { + "message": "Ativar" + }, + "homeButtonTitle": { + "message": "Ir para o Relatório" + }, + "projectNameLabel": { + "message": "Nome do seu Projeto" + }, + "projectNamePlaceholder": { + "message": "Digite o nome do seu projeto" + }, + "githubUsernameLabel": { + "message": "Seu nome de usuário do GitHub" + }, + "githubUsernamePlaceholder": { + "message": "Necessário para buscar suas contribuições" + }, + "contributionsLabel": { + "message": "Buscar suas contribuições de:" + }, + "last7DaysLabel": { + "message": "Últimos 7 dias" + }, + "last1DayLabel": { + "message": "Ontem" + }, + "startDateLabel": { + "message": "Data de Início:" + }, + "endDateLabel": { + "message": "Data de Fim:" + }, + "showOpenClosedLabel": { + "message": "Mostrar status Aberto/Fechado" + }, + "blockersLabel": { + "message": "O que está impedindo seu progresso?" + }, + "blockersPlaceholder": { + "message": "Digite o motivo" + }, + "scrumReportLabel": { + "message": "Relatório Scrum" + }, + "generateReportButton": { + "message": "Gerar Relatório" + }, + "copyReportButton": { + "message": "Copiar Relatório" + }, + "settingsOrgNameLabel": { + "message": "Nome da Organização" + }, + "settingsOrgNamePlaceholder": { + "message": "Digite o nome da organização" + }, + "setButton": { + "message": "Definir" + }, + "githubTokenLabel": { + "message": "Seu Token do GitHub" + }, + "githubTokenPlaceholder": { + "message": "Necessário para solicitações autenticadas" + }, + "showCommitsLabel": { + "message": "Mostrar commits em PRs abertos no período" + }, + "cacheTTLLabel": { + "message": "Digite o TTL do cache" + }, + "cacheTTLUnit": { + "message": "(em minutos)" + }, + "cacheTTLPlaceholder": { + "message": "Digite o TTL do cache em minutos (Padrão: 10)" + }, + "refreshDataButton": { + "message": "Atualizar Dados (ignorar cache)" + }, + "refreshDataInfo": { + "message": "Use este botão para buscar dados novos imediatamente." + }, + "noteTitle": { + "message": "Nota:" + }, + "viewCodeButton": { + "message": "Ver código" + }, + "reportIssueButton": { + "message": "Relatar um problema" + }, + "repoFilterLabel": { + "message": "Filtrar por repositórios" + }, + "enableFilteringLabel": { + "message": "Ativar filtragem" + }, + "tokenRequiredWarning": { + "message": "Um token do GitHub é necessário para filtrar repositórios. Adicione um nas configurações." + }, + "repoSearchPlaceholder": { + "message": "Buscar um repositório para adicionar..." + }, + "repoPlaceholder": { + "message": "Nenhum repositório selecionado (todos serão incluídos)" + }, + "repoCountNone": { + "message": "0 repositórios selecionados" + }, + "repoCount": { + "message": "$1 repositórios selecionados" + }, + "repoLoading": { + "message": "Carregando repositórios..." + }, + "repoLoaded": { + "message": "$1 repositórios carregados." + }, + "repoNotFound": { + "message": "Nenhum repositório encontrado" + }, + "repoRefetching": { + "message": "Recarregando repositórios..." + }, + "repoLoadFailed": { + "message": "Falha ao carregar repositórios." + }, + "repoTokenPrivate": { + "message": "O token é inválido ou não tem permissão para repositórios privados." + }, + "repoRefetchFailed": { + "message": "Falha ao recarregar repositórios." + }, + "orgSetMessage": { + "message": "Organização definida com sucesso." + }, + "orgClearedMessage": { + "message": "Organização removida. Clique em 'Gerar Relatório' para buscar todas as atividades." + }, + "orgNotFoundMessage": { + "message": "Organização não encontrada no GitHub." + }, + "orgValidationErrorMessage": { + "message": "Erro ao validar a organização." + }, + "refreshingButton": { + "message": "Atualizando..." + }, + "cacheClearFailed": { + "message": "Falha ao limpar o cache." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Gerando..." + }, + "copiedButton": { + "message": "Copiado!" + }, + "orgChangedMessage": { + "message": "Organização alterada. Clique em 'Gerar Relatório' para buscar as atividades do GitHub." + }, + "cacheClearedMessage": { + "message": "Cache limpo com sucesso. Clique em 'Gerar Relatório' para buscar dados novos." + }, + "cacheClearedButton": { + "message": "Cache Limpo!" + }, + "extensionDisabledMessage": { + "message": "A extensão está desativada. Ative-a nos ajustes para gerar relatórios scrum." + }, + "notePoint1": { + "message": "Os PRs buscados são baseados na revisão mais recente de qualquer contribuidor. Se você revisou um PR há 10 dias e outra pessoa o revisou há 2 dias, ele ainda aparecerá na sua atividade da última semana." + }, + "notePoint2": { + "message": "Observe que algumas discrepâncias podem ocorrer no Scrum gerado. Recomendamos revisar e editar manualmente o relatório para garantir a precisão antes de compartilhar." + }, + "noteSeeIssue": { + "message": "Veja este issue" + }, + "platformLabel": { + "message": "Plataforma" + }, + "usernameLabel": { + "message": "Seu nome de usuário" + } } \ No newline at end of file diff --git a/src/_locales/ru/messages.json b/src/_locales/ru/messages.json index c2bb752..ddff5f3 100644 --- a/src/_locales/ru/messages.json +++ b/src/_locales/ru/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Сообщайте о своем прогрессе в разработке, автоматически получая данные о вашей активности в Git за выбранный период." }, - "disableLabel": { "message": "Отключить" }, - "enableLabel": { "message": "Включить" }, - "homeButtonTitle": { "message": "Перейти к отчету" }, - "projectNameLabel": { "message": "Название вашего проекта" }, - "projectNamePlaceholder": { "message": "Введите название вашего проекта" }, - "githubUsernameLabel": { "message": "Ваше имя пользователя GitHub" }, - "githubUsernamePlaceholder": { "message": "Требуется для получения ваших вкладов" }, - "contributionsLabel": { "message": "Получить ваши вклады за:" }, - "last7DaysLabel": { "message": "Последние 7 дней" }, - "last1DayLabel": { "message": "Вчера" }, - "startDateLabel": { "message": "Дата начала:" }, - "endDateLabel": { "message": "Дата окончания:" }, - "showOpenClosedLabel": { "message": "Показывать статус Открыто/Закрыто" }, - "blockersLabel": { "message": "Что мешает вашему прогрессу?" }, - "blockersPlaceholder": { "message": "Введите причину" }, - "scrumReportLabel": { "message": "Scrum-отчет" }, - "generateReportButton": { "message": "Создать отчет" }, - "copyReportButton": { "message": "Копировать отчет" }, - "settingsOrgNameLabel": { "message": "Название организации" }, - "settingsOrgNamePlaceholder": { "message": "Введите название организации" }, - "setButton": { "message": "Установить" }, - "githubTokenLabel": { "message": "Ваш токен GitHub" }, - "githubTokenPlaceholder": { "message": "Требуется для аутентифицированных запросов" }, - "showCommitsLabel": { "message": "Показывать коммиты в открытых PR за указанный период" }, - "cacheTTLLabel": { "message": "Введите TTL кэша" }, - "cacheTTLUnit": { "message": "(в минутах)" }, - "cacheTTLPlaceholder": { "message": "Укажите TTL кэша в минутах (по умолчанию: 10)" }, - "refreshDataButton": { "message": "Обновить данные (в обход кэша)" }, - "refreshDataInfo": { "message": "Используйте эту кнопку для немедленного получения свежих данных." }, - "noteTitle": { "message": "Примечание:" }, - "viewCodeButton": { "message": "Посмотреть код" }, - "reportIssueButton": { "message": "Сообщить о проблеме" }, - "repoFilterLabel": { "message": "Фильтровать по репозиториям" }, - "enableFilteringLabel": { "message": "Включить фильтрацию" }, - "tokenRequiredWarning": { "message": "Для фильтрации репозиториев требуется токен GitHub. Пожалуйста, добавьте его в настройках." }, - "repoSearchPlaceholder": { "message": "Найти репозиторий для добавления..." }, - "repoPlaceholder": { "message": "Репозитории не выбраны (будут включены все)" }, - "repoCountNone": { "message": "0 репозиториев выбрано" }, - "repoCount": { "message": "$1 репозиториев выбрано" }, - "repoLoading": { "message": "Загрузка репозиториев..." }, - "repoLoaded": { "message": "Загружено $1 репозиториев." }, - "repoNotFound": { "message": "Репозитории не найдены" }, - "repoRefetching": { "message": "Перезагрузка репозиториев..." }, - "repoLoadFailed": { "message": "Не удалось загрузить репозитории." }, - "repoTokenPrivate": { "message": "Токен недействителен или не имеет прав для частных репозиториев." }, - "repoRefetchFailed": { "message": "Не удалось перезагрузить репозитории." }, - "orgSetMessage": { "message": "Организация успешно установлена." }, - "orgClearedMessage": { "message": "Организация удалена. Нажмите 'Создать отчет', чтобы получить все действия." }, - "orgNotFoundMessage": { "message": "Организация не найдена на GitHub." }, - "orgValidationErrorMessage": { "message": "Ошибка при проверке организации." }, - "refreshingButton": { "message": "Обновление..." }, - "cacheClearFailed": { "message": "Не удалось очистить кэш." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Создание..." }, - "copiedButton": { "message": "Скопировано!" }, - "orgChangedMessage": { "message": "Организация изменена. Нажмите 'Создать отчет', чтобы получить данные о действиях в GitHub." }, - "cacheClearedMessage": { "message": "Кэш успешно очищен. Нажмите 'Создать отчет', чтобы получить свежие данные." }, - "cacheClearedButton": { "message": "Кэш очищен!" }, - "extensionDisabledMessage": { "message": "Расширение отключено. Включите его в настройках, чтобы создавать scrum-отчеты." }, - "notePoint1": { "message": "Полученные PR основаны на последнем ревью любого участника. Если вы сделали ревью PR 10 дней назад, а кто-то другой — 2 дня назад, он все равно появится в вашей активности за последнюю неделю." }, - "notePoint2": { "message": "Обратите внимание, что в сгенерированном Scrum могут быть неточности. Мы рекомендуем вручную проверять и редактировать отчет для обеспечения точности перед отправкой." }, - "noteSeeIssue": { "message": "Смотрите эту проблему" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Сообщайте о своем прогрессе в разработке, автоматически получая данные о вашей активности в Git за выбранный период." + }, + "disableLabel": { + "message": "Отключить" + }, + "enableLabel": { + "message": "Включить" + }, + "homeButtonTitle": { + "message": "Перейти к отчету" + }, + "projectNameLabel": { + "message": "Название вашего проекта" + }, + "projectNamePlaceholder": { + "message": "Введите название вашего проекта" + }, + "githubUsernameLabel": { + "message": "Ваше имя пользователя GitHub" + }, + "githubUsernamePlaceholder": { + "message": "Требуется для получения ваших вкладов" + }, + "contributionsLabel": { + "message": "Получить ваши вклады за:" + }, + "last7DaysLabel": { + "message": "Последние 7 дней" + }, + "last1DayLabel": { + "message": "Вчера" + }, + "startDateLabel": { + "message": "Дата начала:" + }, + "endDateLabel": { + "message": "Дата окончания:" + }, + "showOpenClosedLabel": { + "message": "Показывать статус Открыто/Закрыто" + }, + "blockersLabel": { + "message": "Что мешает вашему прогрессу?" + }, + "blockersPlaceholder": { + "message": "Введите причину" + }, + "scrumReportLabel": { + "message": "Scrum-отчет" + }, + "generateReportButton": { + "message": "Создать отчет" + }, + "copyReportButton": { + "message": "Копировать отчет" + }, + "settingsOrgNameLabel": { + "message": "Название организации" + }, + "settingsOrgNamePlaceholder": { + "message": "Введите название организации" + }, + "setButton": { + "message": "Установить" + }, + "githubTokenLabel": { + "message": "Ваш токен GitHub" + }, + "githubTokenPlaceholder": { + "message": "Требуется для аутентифицированных запросов" + }, + "showCommitsLabel": { + "message": "Показывать коммиты в открытых PR за указанный период" + }, + "cacheTTLLabel": { + "message": "Введите TTL кэша" + }, + "cacheTTLUnit": { + "message": "(в минутах)" + }, + "cacheTTLPlaceholder": { + "message": "Укажите TTL кэша в минутах (по умолчанию: 10)" + }, + "refreshDataButton": { + "message": "Обновить данные (в обход кэша)" + }, + "refreshDataInfo": { + "message": "Используйте эту кнопку для немедленного получения свежих данных." + }, + "noteTitle": { + "message": "Примечание:" + }, + "viewCodeButton": { + "message": "Посмотреть код" + }, + "reportIssueButton": { + "message": "Сообщить о проблеме" + }, + "repoFilterLabel": { + "message": "Фильтровать по репозиториям" + }, + "enableFilteringLabel": { + "message": "Включить фильтрацию" + }, + "tokenRequiredWarning": { + "message": "Для фильтрации репозиториев требуется токен GitHub. Пожалуйста, добавьте его в настройках." + }, + "repoSearchPlaceholder": { + "message": "Найти репозиторий для добавления..." + }, + "repoPlaceholder": { + "message": "Репозитории не выбраны (будут включены все)" + }, + "repoCountNone": { + "message": "0 репозиториев выбрано" + }, + "repoCount": { + "message": "$1 репозиториев выбрано" + }, + "repoLoading": { + "message": "Загрузка репозиториев..." + }, + "repoLoaded": { + "message": "Загружено $1 репозиториев." + }, + "repoNotFound": { + "message": "Репозитории не найдены" + }, + "repoRefetching": { + "message": "Перезагрузка репозиториев..." + }, + "repoLoadFailed": { + "message": "Не удалось загрузить репозитории." + }, + "repoTokenPrivate": { + "message": "Токен недействителен или не имеет прав для частных репозиториев." + }, + "repoRefetchFailed": { + "message": "Не удалось перезагрузить репозитории." + }, + "orgSetMessage": { + "message": "Организация успешно установлена." + }, + "orgClearedMessage": { + "message": "Организация удалена. Нажмите 'Создать отчет', чтобы получить все действия." + }, + "orgNotFoundMessage": { + "message": "Организация не найдена на GitHub." + }, + "orgValidationErrorMessage": { + "message": "Ошибка при проверке организации." + }, + "refreshingButton": { + "message": "Обновление..." + }, + "cacheClearFailed": { + "message": "Не удалось очистить кэш." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Создание..." + }, + "copiedButton": { + "message": "Скопировано!" + }, + "orgChangedMessage": { + "message": "Организация изменена. Нажмите 'Создать отчет', чтобы получить данные о действиях в GitHub." + }, + "cacheClearedMessage": { + "message": "Кэш успешно очищен. Нажмите 'Создать отчет', чтобы получить свежие данные." + }, + "cacheClearedButton": { + "message": "Кэш очищен!" + }, + "extensionDisabledMessage": { + "message": "Расширение отключено. Включите его в настройках, чтобы создавать scrum-отчеты." + }, + "notePoint1": { + "message": "Полученные PR основаны на последнем ревью любого участника. Если вы сделали ревью PR 10 дней назад, а кто-то другой — 2 дня назад, он все равно появится в вашей активности за последнюю неделю." + }, + "notePoint2": { + "message": "Обратите внимание, что в сгенерированном Scrum могут быть неточности. Мы рекомендуем вручную проверять и редактировать отчет для обеспечения точности перед отправкой." + }, + "noteSeeIssue": { + "message": "Смотрите эту проблему" + }, + "platformLabel": { + "message": "Платформа" + }, + "usernameLabel": { + "message": "Ваше имя пользователя" + } } \ No newline at end of file diff --git a/src/_locales/vi/messages.json b/src/_locales/vi/messages.json index e581808..35dc1e1 100644 --- a/src/_locales/vi/messages.json +++ b/src/_locales/vi/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "Báo cáo tiến độ phát triển của bạn bằng cách tự động lấy hoạt động Git trong một khoảng thời gian đã chọn." }, - "disableLabel": { "message": "Tắt" }, - "enableLabel": { "message": "Bật" }, - "homeButtonTitle": { "message": "Đi đến Báo cáo" }, - "projectNameLabel": { "message": "Tên dự án của bạn" }, - "projectNamePlaceholder": { "message": "Nhập tên dự án của bạn" }, - "githubUsernameLabel": { "message": "Tên người dùng GitHub của bạn" }, - "githubUsernamePlaceholder": { "message": "Bắt buộc để lấy đóng góp của bạn" }, - "contributionsLabel": { "message": "Lấy đóng góp của bạn trong:" }, - "last7DaysLabel": { "message": "7 ngày qua" }, - "last1DayLabel": { "message": "Hôm qua" }, - "startDateLabel": { "message": "Ngày bắt đầu:" }, - "endDateLabel": { "message": "Ngày kết thúc:" }, - "showOpenClosedLabel": { "message": "Hiển thị trạng thái Mở/Đóng" }, - "blockersLabel": { "message": "Điều gì đang cản trở bạn?" }, - "blockersPlaceholder": { "message": "Nhập lý do của bạn" }, - "scrumReportLabel": { "message": "Báo cáo Scrum" }, - "generateReportButton": { "message": "Tạo báo cáo" }, - "copyReportButton": { "message": "Sao chép báo cáo" }, - "settingsOrgNameLabel": { "message": "Tên tổ chức" }, - "settingsOrgNamePlaceholder": { "message": "Nhập tên tổ chức" }, - "setButton": { "message": "Đặt" }, - "githubTokenLabel": { "message": "Mã token GitHub của bạn" }, - "githubTokenPlaceholder": { "message": "Bắt buộc cho các yêu cầu đã xác thực" }, - "showCommitsLabel": { "message": "Hiển thị các commit trong PR đang mở theo khoảng ngày" }, - "cacheTTLLabel": { "message": "Nhập TTL bộ nhớ đệm" }, - "cacheTTLUnit": { "message": "(tính bằng phút)" }, - "cacheTTLPlaceholder": { "message": "Nhập TTL bộ nhớ đệm bằng phút (Mặc định: 10)" }, - "refreshDataButton": { "message": "Làm mới dữ liệu (bỏ qua bộ nhớ đệm)" }, - "refreshDataInfo": { "message": "Sử dụng nút này để lấy dữ liệu mới ngay lập tức." }, - "noteTitle": { "message": "Lưu ý:" }, - "viewCodeButton": { "message": "Xem mã" }, - "reportIssueButton": { "message": "Báo cáo sự cố" }, - "repoFilterLabel": { "message": "Lọc theo kho lưu trữ" }, - "enableFilteringLabel": { "message": "Bật bộ lọc" }, - "tokenRequiredWarning": { "message": "Cần có mã token GitHub để lọc kho lưu trữ. Vui lòng thêm một mã trong cài đặt." }, - "repoSearchPlaceholder": { "message": "Tìm kiếm kho lưu trữ để thêm..." }, - "repoPlaceholder": { "message": "Chưa chọn kho lưu trữ nào (tất cả sẽ được bao gồm)" }, - "repoCountNone": { "message": "0 kho lưu trữ đã chọn" }, - "repoCount": { "message": "$1 kho lưu trữ đã chọn" }, - "repoLoading": { "message": "Đang tải kho lưu trữ..." }, - "repoLoaded": { "message": "Đã tải $1 kho lưu trữ." }, - "repoNotFound": { "message": "Không tìm thấy kho lưu trữ nào" }, - "repoRefetching": { "message": "Đang tải lại kho lưu trữ..." }, - "repoLoadFailed": { "message": "Không thể tải kho lưu trữ." }, - "repoTokenPrivate": { "message": "Mã token không hợp lệ hoặc không có quyền đối với kho lưu trữ riêng tư." }, - "repoRefetchFailed": { "message": "Không thể tải lại kho lưu trữ." }, - "orgSetMessage": { "message": "Tổ chức đã được đặt thành công." }, - "orgClearedMessage": { "message": "Đã xóa tổ chức. Nhấp vào 'Tạo báo cáo' để lấy tất cả hoạt động." }, - "orgNotFoundMessage": { "message": "Không tìm thấy tổ chức trên GitHub." }, - "orgValidationErrorMessage": { "message": "Lỗi khi xác thực tổ chức." }, - "refreshingButton": { "message": "Đang làm mới..." }, - "cacheClearFailed": { "message": "Xóa bộ nhớ đệm thất bại." }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "Đang tạo..." }, - "copiedButton": { "message": "Đã sao chép!" }, - "orgChangedMessage": { "message": "Tổ chức đã thay đổi. Nhấp vào 'Tạo báo cáo' để lấy các hoạt động trên GitHub." }, - "cacheClearedMessage": { "message": "Bộ nhớ đệm đã được xóa thành công. Nhấp vào 'Tạo báo cáo' để lấy dữ liệu mới." }, - "cacheClearedButton": { "message": "Đã xóa bộ nhớ đệm!" }, - "extensionDisabledMessage": { "message": "Tiện ích mở rộng đã bị tắt. Bật nó từ cài đặt để tạo báo cáo scrum." }, - "notePoint1": { "message": "Các PR được lấy dựa trên lần review gần đây nhất của bất kỳ người đóng góp nào. Nếu bạn review một PR 10 ngày trước và người khác review nó 2 ngày trước, nó vẫn sẽ xuất hiện trong hoạt động của bạn trong tuần qua." }, - "notePoint2": { "message": "Xin lưu ý rằng có thể xảy ra một số khác biệt trong Scrum được tạo. Chúng tôi khuyên bạn nên xem xét và chỉnh sửa báo cáo thủ công để đảm bảo tính chính xác trước khi chia sẻ." }, - "noteSeeIssue": { "message": "Xem vấn đề này" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "Báo cáo tiến độ phát triển của bạn bằng cách tự động lấy hoạt động Git trong một khoảng thời gian đã chọn." + }, + "disableLabel": { + "message": "Tắt" + }, + "enableLabel": { + "message": "Bật" + }, + "homeButtonTitle": { + "message": "Đi đến Báo cáo" + }, + "projectNameLabel": { + "message": "Tên dự án của bạn" + }, + "projectNamePlaceholder": { + "message": "Nhập tên dự án của bạn" + }, + "githubUsernameLabel": { + "message": "Tên người dùng GitHub của bạn" + }, + "githubUsernamePlaceholder": { + "message": "Bắt buộc để lấy đóng góp của bạn" + }, + "contributionsLabel": { + "message": "Lấy đóng góp của bạn trong:" + }, + "last7DaysLabel": { + "message": "7 ngày qua" + }, + "last1DayLabel": { + "message": "Hôm qua" + }, + "startDateLabel": { + "message": "Ngày bắt đầu:" + }, + "endDateLabel": { + "message": "Ngày kết thúc:" + }, + "showOpenClosedLabel": { + "message": "Hiển thị trạng thái Mở/Đóng" + }, + "blockersLabel": { + "message": "Điều gì đang cản trở bạn?" + }, + "blockersPlaceholder": { + "message": "Nhập lý do của bạn" + }, + "scrumReportLabel": { + "message": "Báo cáo Scrum" + }, + "generateReportButton": { + "message": "Tạo báo cáo" + }, + "copyReportButton": { + "message": "Sao chép báo cáo" + }, + "settingsOrgNameLabel": { + "message": "Tên tổ chức" + }, + "settingsOrgNamePlaceholder": { + "message": "Nhập tên tổ chức" + }, + "setButton": { + "message": "Đặt" + }, + "githubTokenLabel": { + "message": "Mã token GitHub của bạn" + }, + "githubTokenPlaceholder": { + "message": "Bắt buộc cho các yêu cầu đã xác thực" + }, + "showCommitsLabel": { + "message": "Hiển thị các commit trong PR đang mở theo khoảng ngày" + }, + "cacheTTLLabel": { + "message": "Nhập TTL bộ nhớ đệm" + }, + "cacheTTLUnit": { + "message": "(tính bằng phút)" + }, + "cacheTTLPlaceholder": { + "message": "Nhập TTL bộ nhớ đệm bằng phút (Mặc định: 10)" + }, + "refreshDataButton": { + "message": "Làm mới dữ liệu (bỏ qua bộ nhớ đệm)" + }, + "refreshDataInfo": { + "message": "Sử dụng nút này để lấy dữ liệu mới ngay lập tức." + }, + "noteTitle": { + "message": "Lưu ý:" + }, + "viewCodeButton": { + "message": "Xem mã" + }, + "reportIssueButton": { + "message": "Báo cáo sự cố" + }, + "repoFilterLabel": { + "message": "Lọc theo kho lưu trữ" + }, + "enableFilteringLabel": { + "message": "Bật bộ lọc" + }, + "tokenRequiredWarning": { + "message": "Cần có mã token GitHub để lọc kho lưu trữ. Vui lòng thêm một mã trong cài đặt." + }, + "repoSearchPlaceholder": { + "message": "Tìm kiếm kho lưu trữ để thêm..." + }, + "repoPlaceholder": { + "message": "Chưa chọn kho lưu trữ nào (tất cả sẽ được bao gồm)" + }, + "repoCountNone": { + "message": "0 kho lưu trữ đã chọn" + }, + "repoCount": { + "message": "$1 kho lưu trữ đã chọn" + }, + "repoLoading": { + "message": "Đang tải kho lưu trữ..." + }, + "repoLoaded": { + "message": "Đã tải $1 kho lưu trữ." + }, + "repoNotFound": { + "message": "Không tìm thấy kho lưu trữ nào" + }, + "repoRefetching": { + "message": "Đang tải lại kho lưu trữ..." + }, + "repoLoadFailed": { + "message": "Không thể tải kho lưu trữ." + }, + "repoTokenPrivate": { + "message": "Mã token không hợp lệ hoặc không có quyền đối với kho lưu trữ riêng tư." + }, + "repoRefetchFailed": { + "message": "Không thể tải lại kho lưu trữ." + }, + "orgSetMessage": { + "message": "Tổ chức đã được đặt thành công." + }, + "orgClearedMessage": { + "message": "Đã xóa tổ chức. Nhấp vào 'Tạo báo cáo' để lấy tất cả hoạt động." + }, + "orgNotFoundMessage": { + "message": "Không tìm thấy tổ chức trên GitHub." + }, + "orgValidationErrorMessage": { + "message": "Lỗi khi xác thực tổ chức." + }, + "refreshingButton": { + "message": "Đang làm mới..." + }, + "cacheClearFailed": { + "message": "Xóa bộ nhớ đệm thất bại." + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "Đang tạo..." + }, + "copiedButton": { + "message": "Đã sao chép!" + }, + "orgChangedMessage": { + "message": "Tổ chức đã thay đổi. Nhấp vào 'Tạo báo cáo' để lấy các hoạt động trên GitHub." + }, + "cacheClearedMessage": { + "message": "Bộ nhớ đệm đã được xóa thành công. Nhấp vào 'Tạo báo cáo' để lấy dữ liệu mới." + }, + "cacheClearedButton": { + "message": "Đã xóa bộ nhớ đệm!" + }, + "extensionDisabledMessage": { + "message": "Tiện ích mở rộng đã bị tắt. Bật nó từ cài đặt để tạo báo cáo scrum." + }, + "notePoint1": { + "message": "Các PR được lấy dựa trên lần review gần đây nhất của bất kỳ người đóng góp nào. Nếu bạn review một PR 10 ngày trước và người khác review nó 2 ngày trước, nó vẫn sẽ xuất hiện trong hoạt động của bạn trong tuần qua." + }, + "notePoint2": { + "message": "Xin lưu ý rằng có thể xảy ra một số khác biệt trong Scrum được tạo. Chúng tôi khuyên bạn nên xem xét và chỉnh sửa báo cáo thủ công để đảm bảo tính chính xác trước khi chia sẻ." + }, + "noteSeeIssue": { + "message": "Xem vấn đề này" + }, + "platformLabel": { + "message": "Nền tảng" + }, + "usernameLabel": { + "message": "Tên người dùng của bạn" + } } \ No newline at end of file diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index 321d6b9..d9aed18 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "通过自动获取选定时间段内的 Git 活动来报告您的开发进度。" }, - "disableLabel": { "message": "禁用" }, - "enableLabel": { "message": "启用" }, - "homeButtonTitle": { "message": "前往报告" }, - "projectNameLabel": { "message": "您的项目名称" }, - "projectNamePlaceholder": { "message": "输入您的项目名称" }, - "githubUsernameLabel": { "message": "您的 GitHub 用户名" }, - "githubUsernamePlaceholder": { "message": "获取您的贡献所必需" }, - "contributionsLabel": { "message": "获取您在此期间的贡献:" }, - "last7DaysLabel": { "message": "过去 7 天" }, - "last1DayLabel": { "message": "昨天" }, - "startDateLabel": { "message": "开始日期:" }, - "endDateLabel": { "message": "结束日期:" }, - "showOpenClosedLabel": { "message": "显示“打开/关闭”状态" }, - "blockersLabel": { "message": "什么阻碍了您的进展?" }, - "blockersPlaceholder": { "message": "输入您的原因" }, - "scrumReportLabel": { "message": "Scrum 报告" }, - "generateReportButton": { "message": "生成报告" }, - "copyReportButton": { "message": "复制报告" }, - "settingsOrgNameLabel": { "message": "组织名称" }, - "settingsOrgNamePlaceholder": { "message": "输入组织名称" }, - "setButton": { "message": "设置" }, - "githubTokenLabel": { "message": "您的 GitHub 令牌" }, - "githubTokenPlaceholder": { "message": "进行身份验证请求所必需" }, - "showCommitsLabel": { "message": "显示日期范围内在进行中 PR 的提交" }, - "cacheTTLLabel": { "message": "输入缓存 TTL" }, - "cacheTTLUnit": { "message": "(分钟)" }, - "cacheTTLPlaceholder": { "message": "以分钟为单位输入缓存 TTL(默认为 10)" }, - "refreshDataButton": { "message": "刷新数据(绕过缓存)" }, - "refreshDataInfo": { "message": "使用此按钮立即获取新数据。" }, - "noteTitle": { "message": "注意:" }, - "viewCodeButton": { "message": "查看代码" }, - "reportIssueButton": { "message": "报告问题" }, - "repoFilterLabel": { "message": "按仓库筛选" }, - "enableFilteringLabel": { "message": "启用筛选" }, - "tokenRequiredWarning": { "message": "仓库筛选需要GitHub令牌。请在设置中添加一个。" }, - "repoSearchPlaceholder": { "message": "搜索要添加的仓库..." }, - "repoPlaceholder": { "message": "未选择任何仓库(将包含所有仓库)" }, - "repoCountNone": { "message": "已选择 0 个仓库" }, - "repoCount": { "message": "已选择 $1 个仓库" }, - "repoLoading": { "message": "正在加载仓库..." }, - "repoLoaded": { "message": "已加载 $1 个仓库。" }, - "repoNotFound": { "message": "未找到仓库" }, - "repoRefetching": { "message": "正在重新加载仓库..." }, - "repoLoadFailed": { "message": "加载仓库失败。" }, - "repoTokenPrivate": { "message": "令牌无效或没有私有仓库的权限。" }, - "repoRefetchFailed": { "message": "重新加载仓库失败。" }, - "orgSetMessage": { "message": "组织设置成功。" }, - "orgClearedMessage": { "message": "组织已清除。点击“生成报告”以获取所有活动。" }, - "orgNotFoundMessage": { "message": "在GitHub上找不到组织。" }, - "orgValidationErrorMessage": { "message": "验证组织时出错。" }, - "refreshingButton": { "message": "正在刷新..." }, - "cacheClearFailed": { "message": "清除缓存失败。" }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "正在生成..." }, - "copiedButton": { "message": "已复制!" }, - "orgChangedMessage": { "message": "组织已更改。单击“生成报告”按钮以获取 GitHub 活动。" }, - "cacheClearedMessage": { "message": "缓存已成功清除。单击“生成报告”以获取新数据。" }, - "cacheClearedButton": { "message": "缓存已清除!" }, - "extensionDisabledMessage": { "message": "扩展已禁用。请在设置中启用它以生成 Scrum 报告。" }, - "notePoint1": { "message": "获取的 PR 基于任何贡献者的最新审查。如果您在 10 天前审查了某个 PR,而其他人在 2 天前审查了它,它仍将出现在您上周的活动中。" }, - "notePoint2": { "message": "请注意,生成的 Scrum 报告中可能会出现一些差异。我们建议在分享前手动审查和编辑报告以确保准确性。" }, - "noteSeeIssue": { "message": "查看此 issue" } -} + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "通过自动获取选定时间段内的 Git 活动来报告您的开发进度。" + }, + "disableLabel": { + "message": "禁用" + }, + "enableLabel": { + "message": "启用" + }, + "homeButtonTitle": { + "message": "前往报告" + }, + "projectNameLabel": { + "message": "您的项目名称" + }, + "projectNamePlaceholder": { + "message": "输入您的项目名称" + }, + "githubUsernameLabel": { + "message": "您的 GitHub 用户名" + }, + "githubUsernamePlaceholder": { + "message": "获取您的贡献所必需" + }, + "contributionsLabel": { + "message": "获取您在此期间的贡献:" + }, + "last7DaysLabel": { + "message": "过去 7 天" + }, + "last1DayLabel": { + "message": "昨天" + }, + "startDateLabel": { + "message": "开始日期:" + }, + "endDateLabel": { + "message": "结束日期:" + }, + "showOpenClosedLabel": { + "message": "显示“打开/关闭”状态" + }, + "blockersLabel": { + "message": "什么阻碍了您的进展?" + }, + "blockersPlaceholder": { + "message": "输入您的原因" + }, + "scrumReportLabel": { + "message": "Scrum 报告" + }, + "generateReportButton": { + "message": "生成报告" + }, + "copyReportButton": { + "message": "复制报告" + }, + "settingsOrgNameLabel": { + "message": "组织名称" + }, + "settingsOrgNamePlaceholder": { + "message": "输入组织名称" + }, + "setButton": { + "message": "设置" + }, + "githubTokenLabel": { + "message": "您的 GitHub 令牌" + }, + "githubTokenPlaceholder": { + "message": "进行身份验证请求所必需" + }, + "showCommitsLabel": { + "message": "显示日期范围内在进行中 PR 的提交" + }, + "cacheTTLLabel": { + "message": "输入缓存 TTL" + }, + "cacheTTLUnit": { + "message": "(分钟)" + }, + "cacheTTLPlaceholder": { + "message": "以分钟为单位输入缓存 TTL(默认为 10)" + }, + "refreshDataButton": { + "message": "刷新数据(绕过缓存)" + }, + "refreshDataInfo": { + "message": "使用此按钮立即获取新数据。" + }, + "noteTitle": { + "message": "注意:" + }, + "viewCodeButton": { + "message": "查看代码" + }, + "reportIssueButton": { + "message": "报告问题" + }, + "repoFilterLabel": { + "message": "按仓库筛选" + }, + "enableFilteringLabel": { + "message": "启用筛选" + }, + "tokenRequiredWarning": { + "message": "仓库筛选需要GitHub令牌。请在设置中添加一个。" + }, + "repoSearchPlaceholder": { + "message": "搜索要添加的仓库..." + }, + "repoPlaceholder": { + "message": "未选择任何仓库(将包含所有仓库)" + }, + "repoCountNone": { + "message": "已选择 0 个仓库" + }, + "repoCount": { + "message": "已选择 $1 个仓库" + }, + "repoLoading": { + "message": "正在加载仓库..." + }, + "repoLoaded": { + "message": "已加载 $1 个仓库。" + }, + "repoNotFound": { + "message": "未找到仓库" + }, + "repoRefetching": { + "message": "正在重新加载仓库..." + }, + "repoLoadFailed": { + "message": "加载仓库失败。" + }, + "repoTokenPrivate": { + "message": "令牌无效或没有私有仓库的权限。" + }, + "repoRefetchFailed": { + "message": "重新加载仓库失败。" + }, + "orgSetMessage": { + "message": "组织设置成功。" + }, + "orgClearedMessage": { + "message": "组织已清除。点击“生成报告”以获取所有活动。" + }, + "orgNotFoundMessage": { + "message": "在GitHub上找不到组织。" + }, + "orgValidationErrorMessage": { + "message": "验证组织时出错。" + }, + "refreshingButton": { + "message": "正在刷新..." + }, + "cacheClearFailed": { + "message": "清除缓存失败。" + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "正在生成..." + }, + "copiedButton": { + "message": "已复制!" + }, + "orgChangedMessage": { + "message": "组织已更改。单击“生成报告”按钮以获取 GitHub 活动。" + }, + "cacheClearedMessage": { + "message": "缓存已成功清除。单击“生成报告”以获取新数据。" + }, + "cacheClearedButton": { + "message": "缓存已清除!" + }, + "extensionDisabledMessage": { + "message": "扩展已禁用。请在设置中启用它以生成 Scrum 报告。" + }, + "notePoint1": { + "message": "获取的 PR 基于任何贡献者的最新审查。如果您在 10 天前审查了某个 PR,而其他人在 2 天前审查了它,它仍将出现在您上周的活动中。" + }, + "notePoint2": { + "message": "请注意,生成的 Scrum 报告中可能会出现一些差异。我们建议在分享前手动审查和编辑报告以确保准确性。" + }, + "noteSeeIssue": { + "message": "查看此 issue" + }, + "platformLabel": { + "message": "平台" + }, + "usernameLabel": { + "message": "您的用户名" + } +} \ No newline at end of file diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index 1b5a9c5..eb4ffd0 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -1,66 +1,200 @@ { - "appName": { "message": "Scrum Helper" }, - "appDescription": { "message": "透過自動擷取選定時間段內的 Git 活動來報告您的開發進度。" }, - "disableLabel": { "message": "停用" }, - "enableLabel": { "message": "啟用" }, - "homeButtonTitle": { "message": "前往報告" }, - "projectNameLabel": { "message": "您的專案名稱" }, - "projectNamePlaceholder": { "message": "輸入您的專案名稱" }, - "githubUsernameLabel": { "message": "您的 GitHub 使用者名稱" }, - "githubUsernamePlaceholder": { "message": "擷取您的貢獻所必需" }, - "contributionsLabel": { "message": "擷取您在此期間的貢獻:" }, - "last7DaysLabel": { "message": "過去 7 天" }, - "last1DayLabel": { "message": "昨天" }, - "startDateLabel": { "message": "開始日期:" }, - "endDateLabel": { "message": "結束日期:" }, - "showOpenClosedLabel": { "message": "顯示「開啟/關閉」狀態" }, - "blockersLabel": { "message": "什麼阻礙了您的進度?" }, - "blockersPlaceholder": { "message": "輸入您的原因" }, - "scrumReportLabel": { "message": "Scrum 報告" }, - "generateReportButton": { "message": "產生報告" }, - "copyReportButton": { "message": "複製報告" }, - "settingsOrgNameLabel": { "message": "組織名稱" }, - "settingsOrgNamePlaceholder": { "message": "輸入組織名稱" }, - "setButton": { "message": "設定" }, - "githubTokenLabel": { "message": "您的 GitHub 權杖" }, - "githubTokenPlaceholder": { "message": "進行身分驗證請求所必需" }, - "showCommitsLabel": { "message": "顯示日期範圍內在進行中 PR 的提交" }, - "cacheTTLLabel": { "message": "輸入快取 TTL" }, - "cacheTTLUnit": { "message": "(分鐘)" }, - "cacheTTLPlaceholder": { "message": "以分鐘為單位輸入快取 TTL(預設為 10)" }, - "refreshDataButton": { "message": "重新整理資料(繞過快取)" }, - "refreshDataInfo": { "message": "使用此按鈕立即擷取新資料。" }, - "noteTitle": { "message": "注意:" }, - "viewCodeButton": { "message": "檢視程式碼" }, - "reportIssueButton": { "message": "回報問題" }, - "repoFilterLabel": { "message": "按儲存庫篩選" }, - "enableFilteringLabel": { "message": "啟用篩選" }, - "tokenRequiredWarning": { "message": "儲存庫篩選需要GitHub權杖。請在設定中新增一個。" }, - "repoSearchPlaceholder": { "message": "搜尋要新增的儲存庫..." }, - "repoPlaceholder": { "message": "未選擇任何儲存庫(將包含所有儲存庫)" }, - "repoCountNone": { "message": "已選擇 0 個儲存庫" }, - "repoCount": { "message": "已選擇 $1 個儲存庫" }, - "repoLoading": { "message": "正在載入儲存庫..." }, - "repoLoaded": { "message": "已載入 $1 個儲存庫。" }, - "repoNotFound": { "message": "找不到儲存庫" }, - "repoRefetching": { "message": "正在重新載入儲存庫..." }, - "repoLoadFailed": { "message": "載入儲存庫失敗。" }, - "repoTokenPrivate": { "message": "權杖無效或沒有私有儲存庫的權限。" }, - "repoRefetchFailed": { "message": "重新載入儲存庫失敗。" }, - "orgSetMessage": { "message": "組織設定成功。" }, - "orgClearedMessage": { "message": "組織已清除。點擊「產生報告」以擷取所有活動。" }, - "orgNotFoundMessage": { "message": "在GitHub上找不到組織。" }, - "orgValidationErrorMessage": { "message": "驗證組織時發生錯誤。" }, - "refreshingButton": { "message": "正在重新整理..." }, - "cacheClearFailed": { "message": "清除快取失敗。" }, - "madeWithLoveBy": { "message": "Made with ❤️ by" }, - "generatingButton": { "message": "正在產生..." }, - "copiedButton": { "message": "已複製!" }, - "orgChangedMessage": { "message": "組織已變更。點擊「產生報告」按鈕以擷取 GitHub 活動。" }, - "cacheClearedMessage": { "message": "快取已成功清除。點擊「產生報告」以擷取新資料。" }, - "cacheClearedButton": { "message": "快取已清除!" }, - "extensionDisabledMessage": { "message": "擴充功能已停用。請在設定中啟用它以產生 Scrum 報告。" }, - "notePoint1": { "message": "擷取的 PR 基於任何貢獻者的最新審查。如果您在 10 天前審查了某個 PR,而其他人在 2 天前審查了它,它仍將出現在您上週的活動中。" }, - "notePoint2": { "message": "請注意,產生的 Scrum 報告中可能會出現一些差異。我們建議在分享前手動審查和編輯報告以確保準確性。" }, - "noteSeeIssue": { "message": "查看此 issue" } + "appName": { + "message": "Scrum Helper" + }, + "appDescription": { + "message": "透過自動擷取選定時間段內的 Git 活動來報告您的開發進度。" + }, + "disableLabel": { + "message": "停用" + }, + "enableLabel": { + "message": "啟用" + }, + "homeButtonTitle": { + "message": "前往報告" + }, + "projectNameLabel": { + "message": "您的專案名稱" + }, + "projectNamePlaceholder": { + "message": "輸入您的專案名稱" + }, + "githubUsernameLabel": { + "message": "您的 GitHub 使用者名稱" + }, + "githubUsernamePlaceholder": { + "message": "擷取您的貢獻所必需" + }, + "contributionsLabel": { + "message": "擷取您在此期間的貢獻:" + }, + "last7DaysLabel": { + "message": "過去 7 天" + }, + "last1DayLabel": { + "message": "昨天" + }, + "startDateLabel": { + "message": "開始日期:" + }, + "endDateLabel": { + "message": "結束日期:" + }, + "showOpenClosedLabel": { + "message": "顯示「開啟/關閉」狀態" + }, + "blockersLabel": { + "message": "什麼阻礙了您的進度?" + }, + "blockersPlaceholder": { + "message": "輸入您的原因" + }, + "scrumReportLabel": { + "message": "Scrum 報告" + }, + "generateReportButton": { + "message": "產生報告" + }, + "copyReportButton": { + "message": "複製報告" + }, + "settingsOrgNameLabel": { + "message": "組織名稱" + }, + "settingsOrgNamePlaceholder": { + "message": "輸入組織名稱" + }, + "setButton": { + "message": "設定" + }, + "githubTokenLabel": { + "message": "您的 GitHub 權杖" + }, + "githubTokenPlaceholder": { + "message": "進行身分驗證請求所必需" + }, + "showCommitsLabel": { + "message": "顯示日期範圍內在進行中 PR 的提交" + }, + "cacheTTLLabel": { + "message": "輸入快取 TTL" + }, + "cacheTTLUnit": { + "message": "(分鐘)" + }, + "cacheTTLPlaceholder": { + "message": "以分鐘為單位輸入快取 TTL(預設為 10)" + }, + "refreshDataButton": { + "message": "重新整理資料(繞過快取)" + }, + "refreshDataInfo": { + "message": "使用此按鈕立即擷取新資料。" + }, + "noteTitle": { + "message": "注意:" + }, + "viewCodeButton": { + "message": "檢視程式碼" + }, + "reportIssueButton": { + "message": "回報問題" + }, + "repoFilterLabel": { + "message": "按儲存庫篩選" + }, + "enableFilteringLabel": { + "message": "啟用篩選" + }, + "tokenRequiredWarning": { + "message": "儲存庫篩選需要GitHub權杖。請在設定中新增一個。" + }, + "repoSearchPlaceholder": { + "message": "搜尋要新增的儲存庫..." + }, + "repoPlaceholder": { + "message": "未選擇任何儲存庫(將包含所有儲存庫)" + }, + "repoCountNone": { + "message": "已選擇 0 個儲存庫" + }, + "repoCount": { + "message": "已選擇 $1 個儲存庫" + }, + "repoLoading": { + "message": "正在載入儲存庫..." + }, + "repoLoaded": { + "message": "已載入 $1 個儲存庫。" + }, + "repoNotFound": { + "message": "找不到儲存庫" + }, + "repoRefetching": { + "message": "正在重新載入儲存庫..." + }, + "repoLoadFailed": { + "message": "載入儲存庫失敗。" + }, + "repoTokenPrivate": { + "message": "權杖無效或沒有私有儲存庫的權限。" + }, + "repoRefetchFailed": { + "message": "重新載入儲存庫失敗。" + }, + "orgSetMessage": { + "message": "組織設定成功。" + }, + "orgClearedMessage": { + "message": "組織已清除。點擊「產生報告」以擷取所有活動。" + }, + "orgNotFoundMessage": { + "message": "在GitHub上找不到組織。" + }, + "orgValidationErrorMessage": { + "message": "驗證組織時發生錯誤。" + }, + "refreshingButton": { + "message": "正在重新整理..." + }, + "cacheClearFailed": { + "message": "清除快取失敗。" + }, + "madeWithLoveBy": { + "message": "Made with ❤️ by" + }, + "generatingButton": { + "message": "正在產生..." + }, + "copiedButton": { + "message": "已複製!" + }, + "orgChangedMessage": { + "message": "組織已變更。點擊「產生報告」按鈕以擷取 GitHub 活動。" + }, + "cacheClearedMessage": { + "message": "快取已成功清除。點擊「產生報告」以擷取新資料。" + }, + "cacheClearedButton": { + "message": "快取已清除!" + }, + "extensionDisabledMessage": { + "message": "擴充功能已停用。請在設定中啟用它以產生 Scrum 報告。" + }, + "notePoint1": { + "message": "擷取的 PR 基於任何貢獻者的最新審查。如果您在 10 天前審查了某個 PR,而其他人在 2 天前審查了它,它仍將出現在您上週的活動中。" + }, + "notePoint2": { + "message": "請注意,產生的 Scrum 報告中可能會出現一些差異。我們建議在分享前手動審查和編輯報告以確保準確性。" + }, + "noteSeeIssue": { + "message": "查看此 issue" + }, + "platformLabel": { + "message": "平台" + }, + "usernameLabel": { + "message": "您的用戶名" + } } \ No newline at end of file diff --git a/src/popup.html b/src/popup.html index 084920d..c2bd8e8 100644 --- a/src/popup.html +++ b/src/popup.html @@ -45,8 +45,7 @@

    Scrum Helper

    - Night Mode + Night Mode

    Report your development progress by auto-fetching your Git @@ -83,7 +82,7 @@

    Scrum
    -

    Platform +

    Platform

    @@ -110,7 +109,7 @@

    Platform

    -

    Your Project Name +

    Your Project Name @@ -124,20 +123,19 @@

    Your Project Name

    -

    Your Username

    +

    Your Username

    -

    Fetch your contributions between:

    +

    Fetch your contributions between:

    @@ -155,14 +153,12 @@

    Your Username

    - - +
    - + -
    @@ -183,7 +179,8 @@

    Your Username