diff --git a/package.json b/package.json index 72937557be..e542e0f4e0 100644 --- a/package.json +++ b/package.json @@ -527,6 +527,8 @@ "@types/node": "*", "@types/node-fetch": "^2.1.4", "@types/query-string": "^6.1.1", + "@types/react": "^16.8.4", + "@types/react-dom": "^16.8.2", "@types/webpack": "^4.4.10", "@types/ws": "^5.1.2", "css-loader": "^0.28.11", @@ -537,6 +539,8 @@ "gulp-util": "^3.0.8", "minimist": "^1.2.0", "mocha": "^5.2.0", + "react": "^16.8.2", + "react-dom": "^16.8.2", "style-loader": "^0.21.0", "svg-inline-loader": "^0.8.0", "ts-loader": "^4.0.1", diff --git a/preview-src/app.tsx b/preview-src/app.tsx new file mode 100644 index 0000000000..4f7df82e7e --- /dev/null +++ b/preview-src/app.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useContext, useState, useEffect } from 'react'; +import { render } from 'react-dom'; +import { Overview } from './overview'; +import PullRequestContext from './context'; +import { PullRequest } from './cache'; + +export function main() { + render( + {pr => } + , document.getElementById('app')); +} + +function Root({ children }) { + const ctx = useContext(PullRequestContext); + const [pr, setPR] = useState(ctx.pr); + useEffect(() => { + ctx.onchange = setPR; + setPR(ctx.pr); + }, []); + return pr ? children(pr) : 'Loading...'; +} \ No newline at end of file diff --git a/preview-src/cache.ts b/preview-src/cache.ts index 4ee2d91eda..31e63b5530 100644 --- a/preview-src/cache.ts +++ b/preview-src/cache.ts @@ -36,19 +36,18 @@ export interface PullRequest { } export function getState(): PullRequest { - return vscode.getState() || {}; + return vscode.getState(); } export function setState(pullRequest: PullRequest): void { let oldPullRequest = getState(); - if (oldPullRequest.number && oldPullRequest.number === pullRequest.number) { - pullRequest = Object.assign(pullRequest, { - pendingCommentText: oldPullRequest.pendingCommentText - }); + if (oldPullRequest && + oldPullRequest.number && oldPullRequest.number === pullRequest.number) { + pullRequest.pendingCommentText = oldPullRequest.pendingCommentText; } - vscode.setState(pullRequest); + if (pullRequest) { vscode.setState(pullRequest); } } export function updateState(data: Partial): void { diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx new file mode 100644 index 0000000000..3d8f279ba8 --- /dev/null +++ b/preview-src/comment.tsx @@ -0,0 +1,195 @@ +import * as React from 'react'; +import { useContext, useState, useEffect, useRef } from 'react'; + +import Markdown from './markdown'; +import { Spaced, nbsp } from './space'; +import { Avatar, AuthorLink } from './user'; +import Timestamp from './timestamp'; +import { Comment } from '../src/common/comment'; +import { PullRequest } from './cache'; +import PullRequestContext from './context'; +import { editIcon, deleteIcon } from './icon'; + +export type Props = Partial & { + headerInEditMode?: boolean + isPRDescription?: boolean +}; + +export function CommentView(comment: Props) { + const { id, pullRequestReviewId, canEdit, canDelete, bodyHTML, body, isPRDescription } = comment; + const [ bodyMd, setBodyMd ] = useState(body); + const { deleteComment, editComment, setDescription, pr } = useContext(PullRequestContext); + const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id]; + const [inEditMode, setEditMode] = useState(!!currentDraft); + const [showActionBar, setShowActionBar] = useState(false); + + useEffect(() => { + if (body !== bodyMd) { + setBodyMd(body); + } + }, [body]); + + if (inEditMode) { + return React.cloneElement( + comment.headerInEditMode + ? : <>, {}, [ + { + if (pr.pendingCommentDrafts) { + delete pr.pendingCommentDrafts[id]; + } + setEditMode(false); + } + } + onSave={ + async text => { + try { + if (isPRDescription) { + await setDescription(text); + } else { + await editComment({ comment: comment as Comment, text }); + } + setBodyMd(text); + } finally { + setEditMode(false); + } + } + } /> + ]); + } + + return setShowActionBar(true)} + onMouseLeave={() => setShowActionBar(false)} + >{ ((canEdit || canDelete) && showActionBar) + ?
+ {canEdit ? : null} + {canDelete ? : null} +
+ : null + } + +
; +} + +type CommentBoxProps = { + for: Partial + header?: React.ReactChild + onMouseEnter?: any + onMouseLeave?: any + children?: any +}; + +function CommentBox({ + for: comment, + onMouseEnter, onMouseLeave, children }: CommentBoxProps) { + const { user, author, createdAt, htmlUrl } = comment; + console.log('comment=', comment) + return
+
+
+ + + + { + createdAt + ? <> + commented{nbsp} + + + : pending + } + +
+ {children} +
+
; +} + +function EditComment({ id, body, onCancel, onSave }: { id: number, body: string, onCancel: () => void, onSave: (body: string) => void}) { + const draftComment = useRef<{body: string, dirty: boolean}>({ body, dirty: false }); + const { updateDraft } = useContext(PullRequestContext); + useEffect(() => { + const interval = setInterval( + () => { + if (draftComment.current.dirty) { + updateDraft(id, draftComment.current.body); + draftComment.current.dirty = false; + } + }, + 500); + return () => clearInterval(interval); + }); + return
{ + event.preventDefault(); + const { markdown }: any = event.target; + onSave(markdown.value); + } + }> + +
+ + +
+
+ : +

+ {currentTitle} (#{number}) +

; + + return
setShowActionBar(true)} + onMouseLeave={() => setShowActionBar(false)}> + {editableTitle} + { + (canEdit && showActionBar && !inEditMode) + ?
+ {canEdit ? : null} +
+ : null + } +
+ + +
+
; +} + +const CheckoutButtons = ({ isCurrentlyCheckedOut }) => { + const { exitReviewMode, checkout } = useContext(PullRequestContext); + if (isCurrentlyCheckedOut) { + return <> + + + ; + } else { + return ; + } +}; + +export function getStatus(state: PullRequestStateEnum) { + if (state === PullRequestStateEnum.Merged) { + return 'Merged'; + } else if (state === PullRequestStateEnum.Open) { + return 'Open'; + } else { + return 'Closed'; + } +} \ No newline at end of file diff --git a/preview-src/icon.tsx b/preview-src/icon.tsx new file mode 100644 index 0000000000..267f56579a --- /dev/null +++ b/preview-src/icon.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export const Icon = ({ className='', src, title }: { className?: string, title?: string, src: string }) => + ; + +export default Icon; + +export const commitIcon = ; +export const mergeIcon = ; +export const editIcon = ; +export const checkIcon = ; +export const plusIcon = ; +export const deleteIcon = ; +export const pendingIcon = ; +export const commentIcon = ; +export const diffIcon = ; diff --git a/preview-src/index.css b/preview-src/index.css index f02f785cd6..3f29029485 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -body { +#app { display: grid; grid-template-columns: 670px auto; } @@ -82,13 +82,22 @@ body .comment-container .avatar-container a { display: flex; } -body .comment-container .avatar-container img.avatar { +body .comment-container .avatar-container img.avatar, +body .comment-container .avatar-container .avatar-icon svg { margin-right: 0; } -body img.avatar { +body img.avatar, +body span.avatar-icon svg { width: 24px; height: 24px; +} + +.vscode-light .avatar-icon { + filter: invert(100%); +} + +body img.avatar { vertical-align: middle; } @@ -106,9 +115,11 @@ body .comment-container.review { width: 100%; display: flex; flex-direction: column; + position: relative; } body .comment-container .review-comment-header { + position: relative; display: flex; width: 100%; box-sizing: border-box; @@ -153,10 +164,18 @@ body .comment-container .review-comment-header { margin-left: 15px; } -.status-item, #status-checks .form-actions { +.status-item, .form-actions { display: flex; } +.form-actions > input[type=submit] { + margin-left: auto; +} + +.status-check-detail-text { + margin-left: 0.7em; +} + #confirm-merge { margin-left: auto; } @@ -240,7 +259,7 @@ body .hidden-focusable { overflow: hidden; } -button { +button, input[type=submit] { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); border-radius: 0px; @@ -253,12 +272,12 @@ button { user-select: none; } -button:focus { +button:focus, input[type=submit]:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; } -button:hover:enabled, button:focus:enabled { +button:hover:enabled, button:focus:enabled, input[type=submit]:focus:enabled { background-color: var(--vscode-button-hoverBackground); cursor: pointer; } @@ -277,6 +296,7 @@ body button.checkedOut { body button.secondary, body button.secondary:hover { -webkit-filter: grayscale(100%); + filter: grayscale(100%); } body button svg { @@ -297,6 +317,7 @@ body button.checkedOut svg { .overview-title { display: flex; + position: relative; } .overview-title h2 { @@ -324,7 +345,8 @@ body button.checkedOut svg { margin-top: 8px; } -.subtitle .avatar { +.subtitle .avatar, +.subtitle .avatar-icon svg { margin-right: 8px; } @@ -462,7 +484,11 @@ body .overview-title .button-group button { } .commit .avatar-container .avatar, -.merged .avatar-container .avatar { +.commit .avatar-container .avatar-icon, +.commit .avatar-container .avatar-icon svg, +.merged .avatar-container .avatar, +.merged .avatar-container .avatar-icon, +.merged .avatar-container .avatar-icon svg { width: 18px; height: 18px; } @@ -611,22 +637,22 @@ textarea:focus { padding: 5px 0; } -.editing-form .form-actions button { - margin-left: 5px; -} - .reply-button { margin-left: auto; margin-right: 0 !important; } -body .comment-form .form-actions { +.form-actions { + display: flex; +} + +.main-comment-form > .form-actions { padding-top: 10px; margin-bottom: 10px; - display: flex; } -body button:disabled { +body button:disabled, +input[type=submit]:disabled { opacity: 0.4; } @@ -934,7 +960,7 @@ code { } @media (max-width: 925px) { - body { + #app { display: block; } @@ -977,4 +1003,50 @@ code { width: auto; margin-right: 4px; } +} + +.icon { + width: 1em; + height: 1em; + font-size: 16px; +} + +.push-right { + margin-left: auto; +} + +.action-bar { + position: absolute; + display: flex; + justify-content: space-between; + z-index: 100; + top: 9px; + right: 9px; +} + +.flex-action-bar { + display: flex; + justify-content: space-between; + z-index: 100; + margin-left: 9px; +} + +.action-bar > button, +.flex-action-bar > button { + margin-left: 4px; + margin-right: 4px; +} + + +.remove-item { + height: 12px; + cursor: pointer; +} + +.title-editing-form { + flex-grow: 1; +} + +.title-editing-form > .form-actions { + margin-left: 0; } \ No newline at end of file diff --git a/preview-src/index.ts b/preview-src/index.ts index 84540b6ccf..d6dc693526 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -3,339 +3,5 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import './index.css'; -import * as debounce from 'debounce'; -import { dateFromNow } from '../src/common/utils'; -import { EventType, isReviewEvent } from '../src/common/timelineEvent'; -import { PullRequestStateEnum } from '../src/github/interface'; -import { getStatus, renderComment, ActionsBar, renderStatusChecks, updatePullRequestState, ElementIds, appendReview, clearTextArea, renderTimelineEvents, renderReviewers, renderLabels } from './pullRequestOverviewRenderer'; - -import { getMessageHandler } from './message'; -import { getState, setState, PullRequest, updateState } from './cache'; - -window.onload = () => { - const pullRequest = getState(); - if (pullRequest && Object.keys(pullRequest).length) { - renderPullRequest(pullRequest); - } -}; - -const messageHandler = getMessageHandler(message => { - switch (message.command) { - case 'pr.initialize': - const pullRequest = message.pullrequest; - setState(pullRequest); - renderPullRequest(pullRequest); - break; - case 'update-state': - updatePullRequestState(message.state); - break; - case 'pr.update-checkout-status': - updateCheckoutButton(message.isCurrentlyCheckedOut); - break; - case 'pr.enable-exit': - (document.getElementById(ElementIds.CheckoutDefaultBranch)).disabled = false; - break; - case 'set-scroll': - window.scrollTo(message.scrollPosition.x, message.scrollPosition.y); - default: - break; - } -}); - -function renderPullRequest(pr: PullRequest): void { - renderTimelineEvents(pr, messageHandler); - setTitleHTML(pr); - setTextArea(); - renderStatusChecks(pr, messageHandler); - renderReviewers(pr, messageHandler); - renderLabels(pr, messageHandler); - updateCheckoutButton(pr.isCurrentlyCheckedOut); - updatePullRequestState(pr.state); - - addEventListeners(pr); -} - -function setTitleHTML(pr: PullRequest): void { - document.getElementById('title')!.innerHTML = ` -
-
-
- - - -
-
-
-
${getStatus(pr.state)}
- - ${pr.author.login} wants to merge changes from ${pr.head} to ${pr.base}. - Created ${dateFromNow(pr.createdAt)} -
-
- `; - - const title = renderTitle(pr); - (document.getElementById('overview-title')! as any).prepend(title); - - renderDescription(pr); -} - -function renderTitle(pr: PullRequest): HTMLElement { - const titleContainer = document.createElement('h2'); - titleContainer.classList.add('title-container'); - - const titleHeader = document.createElement('div'); - titleHeader.classList.add('description-header'); - - const title = document.createElement('span'); - title.classList.add('title-text'); - title.textContent = pr.title; - - const prNumber = document.createElement('span'); - prNumber.innerHTML = `(#${pr.number})`; - - if (pr.canEdit) { - function updateTitle(text: string) { - pr.title = text; - updateState({ title: text }); - title.textContent = text; - } - - const actionsBar = new ActionsBar( - titleContainer, - { - body: pr.title, - id: pr.number.toString() - }, - title, - messageHandler, - updateTitle, - 'pr.edit-title', - undefined, - undefined, - [prNumber] - ); - - const renderedActionsBar = actionsBar.render(); - actionsBar.registerActionBarListeners(); - titleHeader.appendChild(renderedActionsBar); - - if (pr.pendingCommentDrafts && pr.pendingCommentDrafts[pr.number]) { - actionsBar.startEdit(pr.pendingCommentDrafts[pr.number]); - } - - title.addEventListener('click', () => { - actionsBar.startEdit(); - }); - } - - titleContainer.appendChild(titleHeader); - titleContainer.appendChild(title); - titleContainer.appendChild(prNumber); - - return titleContainer; -} - -function renderDescription(pr: PullRequest): void { - const descriptionNode = document.getElementById('description'); - descriptionNode.innerHTML = ''; - const bodyHTML = !pr.body ? 'No description provided' : pr.bodyHTML; - const descriptionElement = renderComment({ - htmlUrl: pr.url, - body: pr.body, - bodyHTML: bodyHTML, - user: pr.author, - event: EventType.Commented, - canEdit: pr.canEdit, - canDelete: false, - id: pr.number, - createdAt: pr.createdAt - }, messageHandler, undefined, { - handler: (text: string) => { - pr.body = text; - updateState({ body: text }); - }, - command: 'pr.edit-description' }); - - descriptionNode.appendChild(descriptionElement); -} - -function addEventListeners(pr: PullRequest): void { - document.getElementById(ElementIds.Checkout)!.addEventListener('click', async () => { - (document.getElementById(ElementIds.Checkout)).disabled = true; - (document.getElementById(ElementIds.Checkout)).innerHTML = 'Checking Out...'; - let result = await messageHandler.postMessage({ command: 'pr.checkout' }); - updateCheckoutButton(result.isCurrentlyCheckedOut); - }); - - // Enable 'Comment' and 'RequestChanges' button only when the user has entered text - let updateStateTimer: number; - document.getElementById(ElementIds.CommentTextArea)!.addEventListener('input', (e) => { - const inputText = (e.target).value; - const { state } = getState(); - (document.getElementById(ElementIds.Reply)).disabled = !inputText; - (document.getElementById(ElementIds.RequestChanges)).disabled = !inputText || state !== PullRequestStateEnum.Open; - - if (updateStateTimer) { - clearTimeout(updateStateTimer); - } - - updateStateTimer = window.setTimeout(() => { - updateState({ pendingCommentText: inputText }); - }, 500); - }); - - document.getElementById(ElementIds.Refresh).addEventListener('click', () => { - messageHandler.postMessage({ - command: 'pr.refresh' - }); - }); - - document.getElementById(ElementIds.Reply)!.addEventListener('click', () => { - submitComment(); - }); - - document.getElementById(ElementIds.Close)!.addEventListener('click', async () => { - (document.getElementById(ElementIds.Close)).disabled = true; - const inputBox = (document.getElementById(ElementIds.CommentTextArea)); - let result = await messageHandler.postMessage({ command: 'pr.close', args: inputBox.value }); - appendComment(result.value); - }); - - const approveButton = document.getElementById(ElementIds.Approve); - if (approveButton) { - approveButton.addEventListener('click', async () => { - (document.getElementById(ElementIds.Approve)).disabled = true; - const inputBox = (document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.approve', - args: inputBox.value - }).then(message => { - // succeed - appendReview(message, messageHandler); - }, err => { - // enable approve button - (document.getElementById(ElementIds.Approve)).disabled = false; - }); - }); - } - - const requestChangesButton = document.getElementById(ElementIds.RequestChanges); - if (requestChangesButton) { - requestChangesButton.addEventListener('click', () => { - (document.getElementById(ElementIds.RequestChanges)).disabled = true; - const inputBox = (document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.request-changes', - args: inputBox.value - }).then(message => { - appendReview(message, messageHandler); - }, err => { - (document.getElementById(ElementIds.RequestChanges)).disabled = false; - }); - }); - } - - document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { - (document.getElementById(ElementIds.CheckoutDefaultBranch)).disabled = true; - messageHandler.postMessage({ - command: 'pr.checkout-default-branch', - args: pr.repositoryDefaultBranch - }); - }); - - window.onscroll = debounce(() => { - messageHandler.postMessage({ - command: 'scroll', - args: { - x: window.scrollX, - y: window.scrollY - } - }); - }, 200); -} - -async function submitComment() { - (document.getElementById(ElementIds.Reply)).disabled = true; - const result = await messageHandler.postMessage({ - command: 'pr.comment', - args: (document.getElementById(ElementIds.CommentTextArea)!).value - }); - - appendComment(result.value); -} - -function appendComment(comment: any) { - comment.event = EventType.Commented; - - const pullRequest = getState(); - let events = pullRequest.events; - events.push(comment); - updateState({ events: events }); - - const newComment = renderComment(comment, messageHandler); - document.getElementById(ElementIds.TimelineEvents)!.appendChild(newComment); - clearTextArea(); -} - -function updateCheckoutButton(isCheckedOut: boolean) { - updateState({ isCurrentlyCheckedOut: isCheckedOut }); - - const checkoutButton = (document.getElementById(ElementIds.Checkout)); - const checkoutMasterButton = (document.getElementById(ElementIds.CheckoutDefaultBranch)); - checkoutButton.disabled = isCheckedOut; - checkoutMasterButton.disabled = false; - const activeIcon = ''; - checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; - - const backButton = (document.getElementById(ElementIds.CheckoutDefaultBranch)); - if (isCheckedOut) { - backButton.classList.remove('hidden'); - checkoutButton.classList.add('checkedOut'); - } else { - backButton.classList.add('hidden'); - checkoutButton.classList.remove('checkedOut'); - } -} - -function setTextArea() { - const { supportsGraphQl, events } = getState(); - const displaySubmitButtonsOnPendingReview = supportsGraphQl && events.some(e => isReviewEvent(e) && e.state.toLowerCase() === 'pending'); - - document.getElementById('comment-form')!.innerHTML = ` -
- - ${ displaySubmitButtonsOnPendingReview - ? '' - : ` - ` - } - -
`; - - const textArea = (document.getElementById(ElementIds.CommentTextArea)!); - textArea.placeholder = 'Leave a comment'; - textArea.addEventListener('keydown', e => { - if (e.keyCode === 65 && e.metaKey) { - (document.getElementById(ElementIds.CommentTextArea)!).select(); - return; - } - - if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { - submitComment(); - return; - } - }); - - let pullRequestCache = getState(); - - if (pullRequestCache.pendingCommentText) { - textArea.value = pullRequestCache.pendingCommentText; - - const replyButton = document.getElementById(ElementIds.Reply)!; - replyButton.disabled = false; - - const requestChangesButton = document.getElementById(ElementIds.RequestChanges)!; - requestChangesButton.disabled = false; - } -} +import { main } from './app'; +addEventListener('load', main); diff --git a/preview-src/markdown.tsx b/preview-src/markdown.tsx new file mode 100644 index 0000000000..a20e5577bb --- /dev/null +++ b/preview-src/markdown.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +import md from './mdRenderer'; +const emoji = require('node-emoji'); + +type MarkdownProps = { src: string } & Record; + +export const Markdown = ({ src, ...others }: MarkdownProps) => +
; + +export default Markdown; \ No newline at end of file diff --git a/preview-src/merge.tsx b/preview-src/merge.tsx new file mode 100644 index 0000000000..6951e8f8df --- /dev/null +++ b/preview-src/merge.tsx @@ -0,0 +1,189 @@ +import * as React from 'react'; +import { PullRequest } from './cache'; +import PullRequestContext from './context'; +import { groupBy } from 'lodash'; +import { useContext, useReducer, useRef, useState, useEffect } from 'react'; +import { PullRequestStateEnum, MergeMethod } from '../src/github/interface'; +import { checkIcon, deleteIcon, pendingIcon } from './icon'; +import { Avatar, } from './user'; +import { nbsp } from './space'; + +export const StatusChecks = (pr: PullRequest) => { + const { state, status, mergeable } = pr; + const [showDetails, toggleDetails] = useReducer( + show => !show, + status.statuses.some(s => s.state === 'failure')) as [boolean, () => void]; + + useEffect(() => { + if (status.statuses.some(s => s.state === 'failure')) { + if (!showDetails) { toggleDetails(); } + } else { + if (showDetails) { toggleDetails(); } + } + }, status.statuses); + + return
{ + state === PullRequestStateEnum.Merged + ? 'Pull request successfully merged' + : + state === PullRequestStateEnum.Closed + ? 'This pull request is closed' + : + <> + { status.statuses.length + ? <> +
+
+ +
{getSummaryLabel(status.statuses)}
+ { + showDetails ? 'Hide' : 'Show' + } +
+ {showDetails ? + + : null} +
+ + : null + } + + { mergeable ? : null} + + }
; +}; + +export default StatusChecks; + +export const MergeStatus = ({ mergeable }: Pick) => +
+ {mergeable ? checkIcon : deleteIcon} +
{ + mergeable + ? 'This branch has no conflicts with the base branch' + : 'This branch has conflicts that must be resolved' + }
+
; + +export const Merge = (pr: PullRequest) => { + const select = useRef(); + const [ selectedMethod, selectMethod ] = useState(null); + + if (selectedMethod) { + return selectMethod(null)} />; + } + + return
+ + {nbsp}using method{nbsp} + +
; +}; + +function ConfirmMerge({pr, method, cancel}: {pr: PullRequest, method: MergeMethod, cancel: () => void}) { + const { merge } = useContext(PullRequestContext); + + return
{ + event.preventDefault(); + const {title, description}: any = event.target; + merge({ + title: title.value, + description: description.value, + method, + }); + } + }> + + +
+ + + +
+
; +} + +const CommentEventView = (event: CommentEvent) => ; + +const MergedEventView = (event: MergedEvent) => +
+
+ {mergeIcon}{nbsp} +
+ +
+ +
+ merged commit + {event.sha.substr(0, 7)} + into + {event.mergeRef} +
+
+ +
; + +// TODO: We should show these, but the pre-React overview page didn't. Add +// support in a separate PR. +const AssignEventView = (event: AssignEvent) => null; diff --git a/preview-src/timestamp.tsx b/preview-src/timestamp.tsx new file mode 100644 index 0000000000..83bfb4de7b --- /dev/null +++ b/preview-src/timestamp.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +import { dateFromNow } from '../src/common/utils'; + +export const Timestamp = ({ + date, + href, +}: { + date: Date | string, + href: string +}) => {dateFromNow(date)}; + +export default Timestamp; \ No newline at end of file diff --git a/preview-src/tsconfig.json b/preview-src/tsconfig.json index ae63f2f017..c666989701 100644 --- a/preview-src/tsconfig.json +++ b/preview-src/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "./dist/", "module": "commonjs", - "target": "es6", + "target": "es2017", "jsx": "react", "sourceMap": true, "strict": false, diff --git a/preview-src/user.tsx b/preview-src/user.tsx new file mode 100644 index 0000000000..695b3ad4f3 --- /dev/null +++ b/preview-src/user.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { PullRequest } from './cache'; +import { Icon } from './icon'; + +export const Avatar = ({ for: author }: { for: Partial }) => + + {author.avatarUrl + ? + : } + ; + +export const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => + {text}; diff --git a/src/common/timelineEvent.ts b/src/common/timelineEvent.ts index 14601de235..6a055ee45f 100644 --- a/src/common/timelineEvent.ts +++ b/src/common/timelineEvent.ts @@ -26,6 +26,7 @@ export interface Committer { } export interface CommentEvent { + id: number; htmlUrl: string; body: string; bodyHTML?: string; @@ -33,11 +34,11 @@ export interface CommentEvent { event: EventType; canEdit?: boolean; canDelete?: boolean; - id: number; createdAt: string; } export interface ReviewEvent { + id: number; event: EventType; comments: Comment[]; submittedAt: string; @@ -47,10 +48,10 @@ export interface ReviewEvent { user: IAccount; authorAssociation: string; state: 'COMMENTED' | 'APPROVED' | 'CHANGES_REQUESTED' | 'PENDING' | 'REQUESTED'; - id: number; } export interface CommitEvent { + id: number; author: IAccount; event: EventType; sha: string; @@ -61,6 +62,7 @@ export interface CommitEvent { } export interface MergedEvent { + id: number; graphNodeId: string; user: IAccount; createdAt: string; @@ -72,6 +74,7 @@ export interface MergedEvent { } export interface AssignEvent { + id: number; event: EventType; user: IAccount; actor: IAccount; diff --git a/src/github/interface.ts b/src/github/interface.ts index f35c8962e0..709fe67578 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -32,7 +32,7 @@ export interface ReviewState { export interface IAccount { login: string; name?: string; - avatarUrl: string; + avatarUrl?: string; url: string; } diff --git a/src/github/pullRequestManager.ts b/src/github/pullRequestManager.ts index 23a08f40a0..fa94af8598 100644 --- a/src/github/pullRequestManager.ts +++ b/src/github/pullRequestManager.ts @@ -9,7 +9,7 @@ import * as Github from '@octokit/rest'; import { CredentialStore } from './credentials'; import { Comment } from '../common/comment'; import { Remote, parseRepositoryRemotes } from '../common/remote'; -import { TimelineEvent, EventType, ReviewEvent as CommonReviewEvent, isReviewEvent, isCommitEvent } from '../common/timelineEvent'; +import { TimelineEvent, EventType, ReviewEvent as CommonReviewEvent, isReviewEvent, isCommitEvent, isAssignEvent, isCommentEvent, isMergedEvent } from '../common/timelineEvent'; import { GitHubRepository } from './githubRepository'; import { IPullRequestsPagingOptions, PRType, ReviewEvent, ITelemetry, IPullRequestEditData, PullRequest, IRawFileChange, IAccount, ILabel, MergeMethodsAvailability } from './interface'; import { PullRequestGitHelper } from './pullRequestGitHelper'; @@ -620,14 +620,20 @@ export class PullRequestManager { } async getReviewRequests(pullRequest: PullRequestModel): Promise { - const { remote, octokit } = await pullRequest.githubRepository.ensure(); + const githubRepository = pullRequest.githubRepository; + const { remote, octokit } = await githubRepository.ensure(); const result = await octokit.pullRequests.getReviewRequests({ owner: remote.owner, repo: remote.repositoryName, number: pullRequest.prNumber }); - return result.data.users.map(user => convertRESTUserToAccount(user)); + let repositoryReturnsAvatar = true + if(result.data.users.length) { + repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(result.data.users[0].avatar_url); + } + + return result.data.users.map(user => convertRESTUserToAccount(user, repositoryReturnsAvatar)); } async getPullRequestComments(pullRequest: PullRequestModel): Promise { @@ -666,7 +672,8 @@ export class PullRequestManager { */ private async getPullRequestReviewComments(pullRequest: PullRequestModel): Promise { Logger.debug(`Fetch comments of PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); - const { remote, octokit } = await (pullRequest as PullRequestModel).githubRepository.ensure(); + const githubRepository = (pullRequest as PullRequestModel).githubRepository; + const { remote, octokit } = await githubRepository.ensure(); const reviewData = await octokit.pullRequests.getComments({ owner: remote.owner, repo: remote.repositoryName, @@ -674,7 +681,13 @@ export class PullRequestManager { per_page: 100 }); Logger.debug(`Fetch comments of PR #${pullRequest.prNumber} - done`, PullRequestManager.ID); - const rawComments = reviewData.data.map(comment => this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(comment), remote)); + + let repositoryReturnsAvatar = true + if(reviewData.data.length) { + repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(reviewData.data[0].user.avatar_url); + } + + const rawComments = reviewData.data.map(comment => this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(comment, repositoryReturnsAvatar), remote)); return rawComments; } @@ -732,6 +745,7 @@ export class PullRequestManager { ret = data.repository.pullRequest.timeline.edges.map((edge: any) => edge.node); let events = parseGraphQLTimelineEvents(ret); await this.addReviewTimelineEventComments(pullRequest, events); + return events; } catch (e) { console.log(e); @@ -749,6 +763,37 @@ export class PullRequestManager { } } + private async ensureTimelineEventAvatars(githubRepository: GitHubRepository, events: TimelineEvent[]): Promise { + if (!events.length) { + return; + } + + let firstAvatarUrl: string | undefined = + events.map(event => { + if (isCommitEvent(event)) { + return event.author.avatarUrl; + } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)) { + return event.user.avatarUrl; + } + }) + .find(avatarUrl => !!avatarUrl); + + let repositoryReturnsAvatar = null; + if (firstAvatarUrl) { + repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(firstAvatarUrl); + } + + if(repositoryReturnsAvatar === false) { + events.forEach(event => { + if (isCommitEvent(event)) { + event.author.avatarUrl = undefined; + } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)) { + event.user.avatarUrl = undefined; + } + }); + } + } + async getIssueComments(pullRequest: PullRequestModel): Promise { Logger.debug(`Fetch issue comments of PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); const { octokit, remote } = await pullRequest.githubRepository.ensure(); @@ -783,7 +828,8 @@ export class PullRequestManager { return this.addCommentToPendingReview(pullRequest, pendingReviewId, body, { inReplyTo: reply_to.graphNodeId }); } - const { octokit, remote } = await pullRequest.githubRepository.ensure(); + const githubRepository = pullRequest.githubRepository; + const { octokit, remote } = await githubRepository.ensure(); try { let ret = await octokit.pullRequests.createCommentReply({ @@ -794,7 +840,9 @@ export class PullRequestManager { in_reply_to: Number(reply_to.id) }); - return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data), remote); + const repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(ret.data.user.avatar_url); + + return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data, repositoryReturnsAvatar), remote); } catch (e) { this.handleError(e); } @@ -934,7 +982,8 @@ export class PullRequestManager { return this.addCommentToPendingReview(pullRequest as PullRequestModel, pendingReviewId, body, { path: commentPath, position }); } - const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); + const githubRepository = (pullRequest as PullRequestModel).githubRepository; + const { octokit, remote } = await githubRepository.ensure(); try { let ret = await octokit.pullRequests.createComment({ @@ -947,7 +996,9 @@ export class PullRequestManager { position: position }); - return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data), remote); + const repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(ret.data.user.avatar_url); + + return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data, repositoryReturnsAvatar), remote); } catch (e) { this.handleError(e); } @@ -1049,7 +1100,7 @@ export class PullRequestManager { // Create PR let { data } = await repo.octokit.pullRequests.create(params); const item = convertRESTPullRequestToRawPullRequest(data); - const repoReturnsAvatar = await repo.ensureRepositoryReturnsAvatar(item.user.avatarUrl); + const repoReturnsAvatar = await repo.ensureRepositoryReturnsAvatar(item.user.avatarUrl!); const pullRequestModel = new PullRequestModel(repo, repo.remote, item, repoReturnsAvatar); const branchNameSeparatorIndex = params.head.indexOf(':'); @@ -1086,7 +1137,8 @@ export class PullRequestManager { return this.editPendingReviewComment(pullRequest, comment.graphNodeId, text); } - const { octokit, remote } = await pullRequest.githubRepository.ensure(); + const githubRepository = pullRequest.githubRepository; + const { octokit, remote } = await githubRepository.ensure(); const ret = await octokit.pullRequests.editComment({ owner: remote.owner, @@ -1095,7 +1147,9 @@ export class PullRequestManager { comment_id: comment.id }); - return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data), remote); + const repositoryReturnsAvatar = await githubRepository.ensureRepositoryReturnsAvatar(ret.data.user.avatar_url); + + return this.addCommentPermissions(convertPullRequestsGetCommentsResponseItemToComment(ret.data, repositoryReturnsAvatar), remote); } catch (e) { throw new Error(formatError(e)); } @@ -1480,6 +1534,8 @@ export class PullRequestManager { // Ensures that pending comments made in reply to other reviews are included for the pending review pendingReview.comments = reviewComments.filter(c => c.isDraft); } + + await this.ensureTimelineEventAvatars(pullRequest.githubRepository, events); } private async fixCommitAttribution(pullRequest: PullRequestModel, events: TimelineEvent[]): Promise { @@ -1512,12 +1568,14 @@ export class PullRequestManager { } }); - return Promise.all([ + await Promise.all([ this.addReviewTimelineEventComments(pullRequest, events), this.fixCommitAttribution(pullRequest, events) - ]).then(_ => { - return events; - }); + ]); + + this.ensureTimelineEventAvatars(pullRequest.githubRepository, events); + + return events; } } diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 18f52e337d..44cda15d88 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -36,7 +36,7 @@ export class PullRequestModel { return undefined; } public get userAvatarUri(): vscode.Uri | undefined { - if (this.prItem) { + if (this.prItem && this._repositoryReturnsAvatar) { let key = this.userAvatar; if (key) { let uri = vscode.Uri.parse(`${key}&s=${64}`); diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 57fc7a7458..76d1b14e17 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -237,7 +237,12 @@ export class PullRequestOverviewPanel { body: this._pullRequest.body, bodyHTML: this._pullRequest.bodyHTML, labels: this._pullRequest.prItem.labels, - author: this._pullRequest.author, + author:{ + login: this._pullRequest.author.login, + name: this._pullRequest.author.name, + avatarUrl: this._pullRequest.userAvatar, + url: this._pullRequest.author.url + }, state: this._pullRequest.state, events: timelineEvents, isCurrentlyCheckedOut: isCurrentlyCheckedOut, @@ -689,18 +694,8 @@ export class PullRequestOverviewPanel { Pull Request #${number} +
-
- -
-
-
-
-
-
`; } diff --git a/src/github/utils.ts b/src/github/utils.ts index 17b8d8e731..b67a42cb09 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -110,11 +110,11 @@ export function updateCommentCommands(vscodeComment: vscode.Comment, commentCont } } -export function convertRESTUserToAccount(user: Octokit.PullRequestsGetAllResponseItemUser): IAccount { +export function convertRESTUserToAccount(user: Octokit.PullRequestsGetAllResponseItemUser, repositoryReturnsAvatar: boolean): IAccount { return { login: user.login, url: user.html_url, - avatarUrl: user.avatar_url + avatarUrl: repositoryReturnsAvatar ? user.avatar_url : undefined }; } @@ -212,7 +212,7 @@ export function convertIssuesCreateCommentResponseToComment(comment: Octokit.Iss }; } -export function convertPullRequestsGetCommentsResponseItemToComment(comment: Octokit.PullRequestsGetCommentsResponseItem | Octokit.PullRequestsEditCommentResponse): Comment { +export function convertPullRequestsGetCommentsResponseItemToComment(comment: Octokit.PullRequestsGetCommentsResponseItem | Octokit.PullRequestsEditCommentResponse, repositoryReturnsAvatar: boolean): Comment { let ret: Comment = { url: comment.url, id: comment.id, @@ -223,7 +223,7 @@ export function convertPullRequestsGetCommentsResponseItemToComment(comment: Oct commitId: comment.commit_id, originalPosition: comment.original_position, originalCommitId: comment.original_commit_id, - user: convertRESTUserToAccount(comment.user), + user: convertRESTUserToAccount(comment.user, repositoryReturnsAvatar), body: comment.body, createdAt: comment.created_at, htmlUrl: comment.html_url, diff --git a/yarn.lock b/yarn.lock index 780205faf1..c02d74b216 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,11 +74,31 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.2.tgz#3840ec6c12556fdda6e0e6d036df853101d732a4" integrity sha512-9NfEUDp3tgRhmoxzTpTo+lq+KIVFxZahuRX0LHF/9IzKHaWuoWsIrrJ61zw5cnnlGINX8lqJzXYfQTOICS5Q+A== +"@types/prop-types@*": + version "15.5.9" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0" + integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ== + "@types/query-string@^6.1.1": version "6.1.1" resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.1.1.tgz#1cfd9209c8dbd91b940192c8a2b7005d2a743e6f" integrity sha512-wRUeF7KN2yxCMw4VoXzPh3GWStbGaiyVjyX22fG7mpSzt6etLvsA2S0g0IuGeXGwVNIlztzVmGP6AxZMmYTQhw== +"@types/react-dom@^16.8.2": + version "16.8.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12" + integrity sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.8.4": + version "16.8.4" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.4.tgz#134307f5266e866d5e7c25e47f31f9abd5b2ea34" + integrity sha512-Mpz1NNMJvrjf0GcDqiK8+YeOydXfD8Mgag3UtqQ5lXYTsMnOiHcKmO48LiSWMb1rSHB9MV/jlgyNzeAVxWMZRQ== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/tapable@*": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" @@ -1365,6 +1385,11 @@ csso@~2.3.1: clap "^1.0.9" source-map "^0.5.3" +csstype@^2.2.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" + integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== + cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2853,6 +2878,11 @@ js-base64@^2.4.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" integrity sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ== +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -3186,6 +3216,13 @@ long@^3.2.0: resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" @@ -4347,6 +4384,15 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +prop-types@^15.6.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -4479,6 +4525,31 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dom@^16.8.2: + version "16.8.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3" + integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.2" + +react-is@^16.8.1: + version "16.8.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.2.tgz#09891d324cad1cb0c1f2d91f70a71a4bee34df0f" + integrity sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q== + +react@^16.8.2: + version "16.8.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c" + integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.2" + "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -4757,6 +4828,14 @@ sax@^1.2.4, sax@~1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +scheduler@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.2.tgz#969eaee2764a51d2e97b20a60963b2546beff8fa" + integrity sha512-qK5P8tHS7vdEMCW5IPyt8v9MJOHqTrOUgPXib7tqm9vh834ibBX5BNhwkplX/0iOzHW5sXyluehYfS9yrkz9+w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"