lib/utils/milestones.js (159 lines of code) (raw):

import { oneLineTrim } from 'common-tags'; import { colors, priorities } from 'lib/const'; import { colourIsLight, hasLabel, hasLabelContainingString } from 'lib/utils'; /* * This function should return the next nearest release * date including if the release date is today. * dayOfWeek: Sunday is 0, Monday is 1 etc... */ export function getNextMilestone({ dayOfWeek = 4, startDate = new Date(), } = {}) { if (startDate.getDay() === dayOfWeek) { return startDate; } const resultDate = new Date(startDate.getTime()); resultDate.setDate( startDate.getDate() + ((7 + dayOfWeek - startDate.getDay() - 1) % 7) + 1, ); return resultDate; } /* * Formats a date object into a milestone format YYYY.MM.DD * Handles zero filling so 2019.1.1 will be 2019.01.01 */ export function formatDateToMilestone(date) { return oneLineTrim`${date.getFullYear()}- ${(date.getMonth() + 1).toString().padStart(2, '0')}- ${date.getDate().toString().padStart(2, '0')}`; } /* * Computes an object with pagination data based on starting day of week and defaulting * to the current date. * */ export function getMilestonePagination({ dayOfWeek = 4, startDate = new Date(), } = {}) { // The nearest release milestone to the starting point. let nextMilestone = getNextMilestone({ dayOfWeek, startDate }); const prev = new Date( nextMilestone.getFullYear(), nextMilestone.getMonth(), nextMilestone.getDate() - 7, ); // Set next Milestone to 7 days time if we're starting on current milestone date already. if ( formatDateToMilestone(startDate) === formatDateToMilestone(nextMilestone) ) { nextMilestone = new Date( nextMilestone.getFullYear(), nextMilestone.getMonth(), nextMilestone.getDate() + 7, ); } // The current milestone closest to today. const currentMilestone = getNextMilestone(dayOfWeek); return { // The milestone before the startDate. prevFromStart: formatDateToMilestone(prev), // The startDate milestone (might not be a typical release day). start: formatDateToMilestone(startDate), // The milestone after the startDate. nextFromStart: formatDateToMilestone(nextMilestone), // The current closest milestone to today. current: formatDateToMilestone(currentMilestone), }; } // Set priority if there's a priority label associated with the issue. export function setIssuePriorityProp(issue) { const labels = (issue.labels && issue.labels.nodes) || []; issue.priority = null; priorities.forEach((priority) => { if (hasLabelContainingString(labels, priority)) { issue.priority = priority; } }); } // Set the repo name directly on the issue. export function setRepoProp(issue) { if (issue.repository && issue.repository.name) { issue.repo = issue.repository.name; } } // Update project info, export function setProjectProps(issue) { issue.hasProject = false; if ( issue.projectCards && issue.projectCards.nodes && issue.projectCards.nodes.length ) { issue.hasProject = true; issue.projectUrl = issue.projectCards.nodes[0].project.url; issue.projectName = issue.projectCards.nodes[0].project.name; } } // Add assignee prop pointing to the login of the first assignee. export function setAssigneeProp(issue) { const labels = (issue.labels && issue.labels.nodes) || []; issue.isContrib = false; issue.assignee = '00_unassigned'; if (issue.assignees.nodes.length) { issue.assignee = issue.assignees.nodes[0].login; } else if (hasLabelContainingString(labels, 'contrib: assigned')) { issue.isContrib = true; issue.assignee = '01_contributor'; } } export function setReviewerDetails(issue) { issue.reviewers = []; const reviewersListSeen = []; if (issue.state === 'CLOSED') { issue.timelineItems.edges.forEach((timelineItem) => { if (!timelineItem.event.source.reviews) { // This is not a pull request item. return; } const { bodyText } = timelineItem.event.source; const issueTestRx = new RegExp(`Fix(?:es)? #${issue.number}`, 'i'); // Only add the review if the PR contains a `Fixes #num` or `Fix #num` line that // matches the original issue. if (issueTestRx.test(bodyText)) { timelineItem.event.source.reviews.edges.forEach( ({ review: { author } }) => { if (!reviewersListSeen.includes(author.login)) { reviewersListSeen.push(author.login); issue.reviewers.push({ author, prLink: timelineItem.event.source.permalink, }); } }, ); } }); } // Quick and dirty way to provide a sortable key for reviewers. issue.reviewersNames = ''; if (issue.reviewers.length) { issue.reviewersNames = issue.reviewers .map((review) => review.author.login) .join('-'); } } export function setStateLabels(issue) { const labels = (issue.labels && issue.labels.nodes) || []; // Define current state of the issue. issue.stateLabel = issue.state.toLowerCase(); issue.stateLabelColor = issue.state === 'CLOSED' ? colors.closed : colors.open; if (issue.state === 'OPEN' && hasLabel(labels, 'state: pull request ready')) { issue.stateLabel = 'PR ready'; issue.stateLabelColor = colors.prReady; } else if (issue.state === 'OPEN' && hasLabel(labels, 'state: in progress')) { issue.stateLabel = 'in progress'; issue.stateLabelColor = colors.inProgress; } else if ( issue.state === 'CLOSED' && hasLabel(labels, 'state: verified fixed') ) { issue.stateLabel = 'verified fixed'; issue.stateLabelColor = colors.verified; } else if (issue.state === 'CLOSED' && hasLabel(labels, 'qa: not needed')) { issue.stateLabel = 'closed QA-'; issue.stateLabelColor = colors.verified; } issue.stateLabelTextColor = colourIsLight(issue.stateLabelColor) ? '#000' : '#fff'; } /* * This function massages the issue data and adds additional properties * to make it easier to display. */ export function formatIssueData(jsonData) { const issues = []; if (jsonData.data && jsonData.data.milestone_issues) { const issueData = jsonData.data.milestone_issues.results; issueData.forEach((item) => { // Set defaults. const { issue } = item; setIssuePriorityProp(issue); setRepoProp(issue); setProjectProps(issue); setStateLabels(issue); setAssigneeProp(issue); setReviewerDetails(issue); issues.push(issue); }); } return issues; }