From ff6a26bc553808add85527ddc4e14454d3df7108 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Thu, 21 Feb 2019 11:22:44 -1000 Subject: [PATCH 01/50] WIP: Bring in React and start converting the overview page. Title bar looks pretty good, actions are not wired. Co-Authored-By: Stanley Goldman --- package.json | 4 ++ preview-src/app.tsx | 8 ++++ preview-src/index.ts | 4 ++ preview-src/message.ts | 1 + preview-src/views.tsx | 53 +++++++++++++++++++++ src/github/pullRequestOverview.ts | 1 + yarn.lock | 79 +++++++++++++++++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 preview-src/app.tsx create mode 100644 preview-src/views.tsx diff --git a/package.json b/package.json index 7dac45e809..271d10ec97 100644 --- a/package.json +++ b/package.json @@ -468,6 +468,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", @@ -478,6 +480,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..8c0bdba77a --- /dev/null +++ b/preview-src/app.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { render } from 'react-dom'; +import { Overview } from './views'; +import { PullRequest } from './cache'; + +export function main(pr: PullRequest) { + render(, document.getElementById('main')); +} diff --git a/preview-src/index.ts b/preview-src/index.ts index 4474dd49ab..d8c5208cf5 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -12,6 +12,7 @@ import md from './mdRenderer'; const emoji = require('node-emoji'); import { getMessageHandler } from './message'; import { getState, setState, PullRequest, updateState } from './cache'; +import { main } from './app'; window.onload = () => { const pullRequest = getState(); @@ -32,6 +33,8 @@ const messageHandler = getMessageHandler(message => { break; case 'pr.update-checkout-status': updateCheckoutButton(message.isCurrentlyCheckedOut); + updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); + renderPullRequest(getState()); break; case 'pr.enable-exit': (document.getElementById(ElementIds.CheckoutDefaultBranch)).disabled = false; @@ -44,6 +47,7 @@ const messageHandler = getMessageHandler(message => { }); function renderPullRequest(pr: PullRequest): void { + main(pr); renderTimelineEvents(pr); setTitleHTML(pr); setTextArea(); diff --git a/preview-src/message.ts b/preview-src/message.ts index 9489c65327..daa26352b5 100644 --- a/preview-src/message.ts +++ b/preview-src/message.ts @@ -45,6 +45,7 @@ export class MessageHandler { // handle message should resolve promises private handleMessage(event: any) { const message: IReplyMessage = event.data; // The json data that the extension sent + console.log('Message:', message); if (message.seq) { // this is a reply let pendingReply = this.pendingReplies[message.seq]; diff --git a/preview-src/views.tsx b/preview-src/views.tsx new file mode 100644 index 0000000000..0f1e9454bd --- /dev/null +++ b/preview-src/views.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { dateFromNow } from '../src/common/utils'; +import { getStatus } from './pullRequestOverviewRenderer'; +import { PullRequest } from './cache'; + +export const Overview = (pr: PullRequest) => + <> + + <hr/> + </>; + +const Avatar = ({ for: author }: { for: PullRequest['author'] }) => + <img className='avatar' src={author.avatarUrl} alt='' />; + +const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => + <a href={author.url}>{text}</a>; + +const spaced = (...things: any[]) => { + const out = new Array(things.length * 2 - 1).fill(' '); + let i = things.length; while (i --> 0) { + out[i * 2] = things[i]; + } + return out; +}; + +export const Title = ({ state, title, head, base, url, createdAt, author, isCurrentlyCheckedOut }: PullRequest) => + author && + <div className='details'> + <div className='overview-title'> + <h2>{title}</h2> + <div className='button-group'> + { + isCurrentlyCheckedOut + ? <button aria-live='polite'>Exit Review Mode</button> + : <button aria-live='polite'>Checkout</button> + } + <button>Refresh</button> + </div> + </div> + <div className='subtitle'> + <div id='status'>{getStatus(state)}</div> + <Avatar for={author} /> + <span className='author'>{spaced( + <AuthorLink for={author} />, + 'wants to merge changes from', + <code>{head}</code>, + 'to', + <code>{base}</code> + )}.</span> + <span className='created-at'>Created + <a href={url} className='timestamp'> {dateFromNow(createdAt)} </a></span> + </div> + </div>; \ No newline at end of file diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index c8a068adaa..5b1ff479d4 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -496,6 +496,7 @@ export class PullRequestOverviewPanel { <title>Pull Request #${number} +
diff --git a/yarn.lock b/yarn.lock index 614d4724a6..ed0a4c41eb 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" @@ -1435,6 +1455,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" @@ -3199,6 +3224,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" @@ -3542,6 +3572,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" @@ -4810,6 +4847,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" @@ -4965,6 +5011,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" @@ -5250,6 +5321,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" From 8dd19992b46c49b5e0d12069af9c5366fdc8ccee Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Thu, 21 Feb 2019 12:03:52 -1000 Subject: [PATCH 02/50] WIP: Spaces are hard. Co-Authored-By: Stanley Goldman --- preview-src/views.tsx | 75 ++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 0f1e9454bd..68b5fcd650 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { dateFromNow } from '../src/common/utils'; import { getStatus } from './pullRequestOverviewRenderer'; import { PullRequest } from './cache'; +import md from './mdRenderer'; export const Overview = (pr: PullRequest) => <> - + <Details {...pr} /> <hr/> </>; @@ -15,17 +16,25 @@ const Avatar = ({ for: author }: { for: PullRequest['author'] }) => const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => <a href={author.url}>{text}</a>; -const spaced = (...things: any[]) => { - const out = new Array(things.length * 2 - 1).fill(' '); - let i = things.length; while (i --> 0) { - out[i * 2] = things[i]; - } - return out; +const Spaced = ({ children }) => { + const count = React.Children.count(children); + return React.createElement(React.Fragment, { + children: React.Children.map(children, (c, i) => + typeof c === 'string' + ? `${i > 0 ? ' ' : ''}${c}${i < count - 1 ? ' ' : ''}` + : c + ) + }); }; -export const Title = ({ state, title, head, base, url, createdAt, author, isCurrentlyCheckedOut }: PullRequest) => - author && +export const Details = (pr: PullRequest) => <div className='details'> + <Header {...pr} /> + <Description {...pr} /> + </div>; + +export const Header = ({ state, title, head, base, url, createdAt, author, isCurrentlyCheckedOut }: PullRequest) => + <> <div className='overview-title'> <h2>{title}</h2> <div className='button-group'> @@ -40,14 +49,42 @@ export const Title = ({ state, title, head, base, url, createdAt, author, isCurr <div className='subtitle'> <div id='status'>{getStatus(state)}</div> <Avatar for={author} /> - <span className='author'>{spaced( - <AuthorLink for={author} />, - 'wants to merge changes from', - <code>{head}</code>, - 'to', - <code>{base}</code> - )}.</span> - <span className='created-at'>Created - <a href={url} className='timestamp'> {dateFromNow(createdAt)} </a></span> + <span className='author'> + <Spaced> + <AuthorLink for={author} /> wants to merge changes + from <code>{head}</code> + to <code>{base}</code> + </Spaced>. + </span> + <span className='created-at'> + <Spaced> + Created + <a href={url} className='timestamp'>{dateFromNow(createdAt)}</a> + </Spaced> + </span> </div> - </div>; \ No newline at end of file + </>; + +const Description = ({ bodyHTML, body }: PullRequest) => + <div className='description-container'>{ + bodyHTML + ? <div className='comment-body' + dangerouslySetInnerHTML={ {__html: bodyHTML }} /> + : + <Markdown className='comment-body' src={body} /> + }</div>; + +const emoji = require('node-emoji'); + +type MarkdownProps = { src: string } & Record<string, any>; + +const Markdown = ({ src, ...others }: MarkdownProps) => + <div dangerouslySetInnerHTML={{ __html: md.render(emoji.emojify(src)) }} {...others} />; + +// const commentBody = document.createElement('div'); +// commentBody.className = 'comment-body'; +// commentBody.innerHTML = pr.bodyHTML ? +// pr.bodyHTML : +// pr.body +// ? md.render(emoji.emojify(pr.body)) +// : '<p><i>No description provided.</i></p>'; From c5b75c66fe6eb69ba6b50dd37b888782fa257987 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 8 Mar 2019 14:18:01 -0800 Subject: [PATCH 03/50] wip --- preview-src/views.tsx | 62 ++++++++++++++++++++++++++---- src/authentication/githubServer.ts | 3 +- src/common/timelineEvent.ts | 7 +++- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 68b5fcd650..d7a76306a8 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -7,11 +7,14 @@ import md from './mdRenderer'; export const Overview = (pr: PullRequest) => <> <Details {...pr} /> + <Timeline events={pr.events} /> <hr/> </>; const Avatar = ({ for: author }: { for: PullRequest['author'] }) => - <img className='avatar' src={author.avatarUrl} alt='' />; + <a className='avatar-link' href={author.url}> + <img className='avatar' src={author.avatarUrl} alt='' /> + </a>; const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => <a href={author.url}>{text}</a>; @@ -81,10 +84,53 @@ type MarkdownProps = { src: string } & Record<string, any>; const Markdown = ({ src, ...others }: MarkdownProps) => <div dangerouslySetInnerHTML={{ __html: md.render(emoji.emojify(src)) }} {...others} />; -// const commentBody = document.createElement('div'); -// commentBody.className = 'comment-body'; -// commentBody.innerHTML = pr.bodyHTML ? -// pr.bodyHTML : -// pr.body -// ? md.render(emoji.emojify(pr.body)) -// : '<p><i>No description provided.</i></p>'; +import { TimelineEvent, isReviewEvent, isCommitEvent, isCommentEvent, isMergedEvent, isAssignEvent, ReviewEvent, CommitEvent, CommentEvent, MergedEvent, AssignEvent } from '../src/common/timelineEvent'; +const Timeline = ({ events }: { events: TimelineEvent[] }) => + <>{ + events.map(event => + // TODO: Maybe make TimelineEvent a tagged union type? + isCommitEvent(event) + ? <Commit key={event.id} {...event} /> + : + isReviewEvent(event) + ? <Review key={event.id} {...event} /> + : + isCommentEvent(event) + ? <Comment key={event.id} {...event} /> + : + isMergedEvent(event) + ? <Merged key={event.id} {...event} /> + : + isAssignEvent(event) + ? <Assign key={event.id} {...event} /> + : null + ) + }</>; + +const commitIconSvg = require('../resources/icons/commit_icon.svg'); +// const mergeIconSvg = require('../resources/icons/merge_icon.svg'); +// const editIcon = require('../resources/icons/edit.svg'); +// const deleteIcon = require('../resources/icons/delete.svg'); +// const checkIcon = require('../resources/icons/check.svg'); +// const dotIcon = require('../resources/icons/dot.svg'); + +const Icon = ({ src }: { src: string }) => + <span dangerouslySetInnerHTML={{ __html: src }} />; + +const Commit = (event: CommitEvent) => + <div className='comment-container commit'> + <div className='commit-message'> + <Icon src={commitIconSvg} /> + <div className='avatar-container'> + <Avatar for={event.author} /> + </div> + <AuthorLink for={event.author} /> + <div className='message'>{event.message}</div> + </div> + <a className='sha' href={event.url}>{event.sha}</a> + </div>; + +const Review = (event: ReviewEvent) => <h1>Review: {event.id}</h1>; +const Comment = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; +const Merged = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; +const Assign = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; diff --git a/src/authentication/githubServer.ts b/src/authentication/githubServer.ts index ffb620e0bd..c01a0ea481 100644 --- a/src/authentication/githubServer.ts +++ b/src/authentication/githubServer.ts @@ -13,7 +13,8 @@ import { onDidChange as onKeychainDidChange, toCanonical, listHosts } from './ke const SCOPES: string = 'read:user user:email repo write:discussion'; const GHE_OPTIONAL_SCOPES: { [key: string]: boolean } = {'write:discussion': true}; -const AUTH_RELAY_SERVER = 'https://vscode-auth.github.com'; +// const AUTH_RELAY_SERVER = 'https://vscode-auth.github.com'; +const AUTH_RELAY_SERVER = 'https://client-auth-staging-14a768b.herokuapp.com'; const CALLBACK_PATH = '/did-authenticate'; const CALLBACK_URI = vscode.version.endsWith('-insider') ? `vscode-insiders://${EXTENSION_ID}${CALLBACK_PATH}` diff --git a/src/common/timelineEvent.ts b/src/common/timelineEvent.ts index ddd2c6ae2f..7d5b8bc1cf 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: string; - 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; From ff4af4d9440f403e5a3e1888b1552094a629986b Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Wed, 13 Mar 2019 17:31:37 -0700 Subject: [PATCH 04/50] WHITESPACE Co-Authored-By: Tilde Ann Thurium <annthurium@github.com> --- preview-src/views.tsx | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index d7a76306a8..9eb5cfbcd9 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -19,12 +19,13 @@ const Avatar = ({ for: author }: { for: PullRequest['author'] }) => const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => <a href={author.url}>{text}</a>; +const nbsp = String.fromCharCode(0xa0) const Spaced = ({ children }) => { const count = React.Children.count(children); return React.createElement(React.Fragment, { children: React.Children.map(children, (c, i) => typeof c === 'string' - ? `${i > 0 ? ' ' : ''}${c}${i < count - 1 ? ' ' : ''}` + ? `${i > 0 ? nbsp : ''}${c}${i < count - 1 ? nbsp : ''}` : c ) }); @@ -130,7 +131,43 @@ const Commit = (event: CommitEvent) => <a className='sha' href={event.url}>{event.sha}</a> </div>; -const Review = (event: ReviewEvent) => <h1>Review: {event.id}</h1>; +const association = ({ authorAssociation }: ReviewEvent, + format=(assoc: string) => `(${assoc.toLowerCase()})`) => + (authorAssociation && authorAssociation !== 'NONE') + ? format(authorAssociation) + : null; +const TotallySpaced = ({ children }) => { + const count = React.Children.count(children); + const out = React.createElement(React.Fragment, { + children: React.Children.map(children, child => [child, ' ']) + .reduce((all, one) => all.concat(one), []) + // React.Children.map(children, (c, i) => + // typeof c === 'string' + // ? `${i > 0 ? ' ' : ''}${c}${i < count - 1 ? ' ' : ''}` + // : c + // ) + }); + console.log('TotallySpaced, bro=', out) + return out +}; + +const Review = (event: ReviewEvent) => + <> + <h1>Review: {event.id}</h1> + <div className='comment-container comment'> + <div className='review-comment-container'> + <div className='review-comment-header'> + <Spaced> + <Avatar for={event.user} /> + <AuthorLink for={event.user} />{association(event)} + reviewed + <a className='timestamp' href={event.htmlUrl}>{dateFromNow(event.submittedAt)}</a> + </Spaced> + </div> + </div> + </div> + </>; + const Comment = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; const Merged = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; const Assign = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; From ab42049bdcd14d025ad8f623d7d126344b6b5c3d Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Wed, 13 Mar 2019 18:35:14 -0700 Subject: [PATCH 05/50] Diffs render pretty well. Co-Authored-By: Tilde Ann Thurium <annthurium@github.com> --- preview-src/pullRequestOverviewRenderer.ts | 3 +- preview-src/tsconfig.json | 2 +- preview-src/views.tsx | 111 ++++++++++++++++----- 3 files changed, 89 insertions(+), 27 deletions(-) diff --git a/preview-src/pullRequestOverviewRenderer.ts b/preview-src/pullRequestOverviewRenderer.ts index 647e542eaf..6c018fa6ea 100644 --- a/preview-src/pullRequestOverviewRenderer.ts +++ b/preview-src/pullRequestOverviewRenderer.ts @@ -856,7 +856,8 @@ class ReviewNode { if (this._review.comments) { const commentBody: HTMLDivElement = document.createElement('div'); commentBody.classList.add('comment-body', 'review-comment-body'); - let groups = groupBy(this._review.comments, comment => comment.path + ':' + (comment.position !== null ? `pos:${comment.position}` : `ori:${comment.originalPosition}`)); + let groups = groupBy(this._review.comments, + comment => comment.path + ':' + (comment.position !== null ? `pos:${comment.position}` : `ori:${comment.originalPosition}`)); for (let path in groups) { let comments = groups[path]; 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/views.tsx b/preview-src/views.tsx index 9eb5cfbcd9..dc4c5bc777 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { dateFromNow } from '../src/common/utils'; +import { Comment } from '../src/common/comment'; import { getStatus } from './pullRequestOverviewRenderer'; import { PullRequest } from './cache'; import md from './mdRenderer'; @@ -91,19 +92,19 @@ const Timeline = ({ events }: { events: TimelineEvent[] }) => events.map(event => // TODO: Maybe make TimelineEvent a tagged union type? isCommitEvent(event) - ? <Commit key={event.id} {...event} /> + ? <CommitEventView key={event.id} {...event} /> : isReviewEvent(event) - ? <Review key={event.id} {...event} /> + ? <ReviewEventView key={event.id} {...event} /> : isCommentEvent(event) - ? <Comment key={event.id} {...event} /> + ? <CommentEventView key={event.id} {...event} /> : isMergedEvent(event) - ? <Merged key={event.id} {...event} /> + ? <MergedEventView key={event.id} {...event} /> : isAssignEvent(event) - ? <Assign key={event.id} {...event} /> + ? <AssignEventView key={event.id} {...event} /> : null ) }</>; @@ -118,7 +119,7 @@ const commitIconSvg = require('../resources/icons/commit_icon.svg'); const Icon = ({ src }: { src: string }) => <span dangerouslySetInnerHTML={{ __html: src }} />; -const Commit = (event: CommitEvent) => +const CommitEventView = (event: CommitEvent) => <div className='comment-container commit'> <div className='commit-message'> <Icon src={commitIconSvg} /> @@ -136,23 +137,22 @@ const association = ({ authorAssociation }: ReviewEvent, (authorAssociation && authorAssociation !== 'NONE') ? format(authorAssociation) : null; -const TotallySpaced = ({ children }) => { - const count = React.Children.count(children); - const out = React.createElement(React.Fragment, { - children: React.Children.map(children, child => [child, ' ']) - .reduce((all, one) => all.concat(one), []) - // React.Children.map(children, (c, i) => - // typeof c === 'string' - // ? `${i > 0 ? ' ' : ''}${c}${i < count - 1 ? ' ' : ''}` - // : c - // ) - }); - console.log('TotallySpaced, bro=', out) - return out -}; -const Review = (event: ReviewEvent) => - <> +import { groupBy } from 'lodash'; +import { DiffHunk, DiffLine } from '../src/common/diffHunk'; + +const positionKey = (comment: Comment) => + comment.position !== null + ? `pos:${comment.position}` + : `ori:${comment.originalPosition}`; + +const groupCommentsByPath = (comments: Comment[]) => + groupBy(comments, + comment => comment.path + ':' + positionKey(comment)); + +const ReviewEventView = (event: ReviewEvent) => { + const comments = groupCommentsByPath(event.comments); + return <> <h1>Review: {event.id}</h1> <div className='comment-container comment'> <div className='review-comment-container'> @@ -164,10 +164,71 @@ const Review = (event: ReviewEvent) => <a className='timestamp' href={event.htmlUrl}>{dateFromNow(event.submittedAt)}</a> </Spaced> </div> + <div className='comment-body review-comment-body'>{ + Object.entries(comments) + .map( + ([key, thread]) => + <div className='diff-container'> + <Diff key={key} hunks={thread[0].diffHunks} path={thread[0].path} /> + ...comments... + </div> + ) + }</div> </div> </div> </>; +}; + +const Diff = ({ hunks, path }: { hunks: DiffHunk[], path: string }) => + <div className='diff'> + <div className='diffHeader'> + <span className='diffPath'>{path}</span> + </div> + {hunks.map(hunk => <Hunk hunk={hunk} />)} + </div>; -const Comment = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; -const Merged = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; -const Assign = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; +const Hunk = ({ hunk, maxLines=4 }: {hunk: DiffHunk, maxLines?: number }) => <>{ + hunk.diffLines.slice(-maxLines) + .map(line => + <div key={keyForDiffLine(line)} className={`diffLine ${getDiffChangeClass(line.type)}`}> + <LineNumber num={line.oldLineNumber} /> + <LineNumber num={line.newLineNumber} /> + <span className='lineContent'>{(line as any)._raw}</span> + </div>) +}</>; + +const keyForDiffLine = (diffLine: DiffLine) => + `${diffLine.oldLineNumber}->${diffLine.newLineNumber}`; + +const LineNumber = ({ num }: { num: number }) => + <span className='lineNumber'>{num > 0 ? num : ' '}</span>; +// const ReviewComment = (c: Comment) => { +// return <div className='comment-body review-comment-body'> + +// </div> +// } + + +const CommentEventView = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; +const MergedEventView = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; +const AssignEventView = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; + +export enum DiffChangeType { + Context, + Add, + Delete, + Control +} + +export function getDiffChangeType(text: string) { + let c = text[0]; + switch (c) { + case ' ': return DiffChangeType.Context; + case '+': return DiffChangeType.Add; + case '-': return DiffChangeType.Delete; + default: return DiffChangeType.Control; + } +} + +const getDiffChangeClass = (type: DiffChangeType) => + DiffChangeType[type].toLowerCase(); \ No newline at end of file From 60309675b0ac3577572e175613e4aa7f74ca9e8b Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Wed, 13 Mar 2019 19:00:56 -0700 Subject: [PATCH 06/50] Review comments are rendering, things looking pretty good. Co-Authored-By: Tilde Ann Thurium <annthurium@github.com> --- preview-src/views.tsx | 101 ++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index dc4c5bc777..3a76aec167 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -63,21 +63,34 @@ export const Header = ({ state, title, head, base, url, createdAt, author, isCur </span> <span className='created-at'> <Spaced> - Created - <a href={url} className='timestamp'>{dateFromNow(createdAt)}</a> + Created <Timestamp date={createdAt} href={url} /> </Spaced> </span> </div> </>; -const Description = ({ bodyHTML, body }: PullRequest) => - <div className='description-container'>{ - bodyHTML - ? <div className='comment-body' - dangerouslySetInnerHTML={ {__html: bodyHTML }} /> - : - <Markdown className='comment-body' src={body} /> - }</div>; +const Timestamp = ({ + date, + href, +}: { + date: Date | string, + href: string +}) => <a href={href} className='timestamp'>{dateFromNow(date)}</a>; + +interface Embodied { + bodyHTML?: string; + body?: string; +} + +const CommentBody = ({ bodyHTML, body }: Embodied) => + bodyHTML + ? <div className='comment-body' + dangerouslySetInnerHTML={ {__html: bodyHTML }} /> + : + <Markdown className='comment-body' src={body} />; + +const Description = (pr: PullRequest) => + <div className='description-container'><CommentBody {...pr} /></div>; const emoji = require('node-emoji'); @@ -152,37 +165,37 @@ const groupCommentsByPath = (comments: Comment[]) => const ReviewEventView = (event: ReviewEvent) => { const comments = groupCommentsByPath(event.comments); - return <> - <h1>Review: {event.id}</h1> - <div className='comment-container comment'> - <div className='review-comment-container'> - <div className='review-comment-header'> - <Spaced> - <Avatar for={event.user} /> - <AuthorLink for={event.user} />{association(event)} - reviewed - <a className='timestamp' href={event.htmlUrl}>{dateFromNow(event.submittedAt)}</a> - </Spaced> - </div> - <div className='comment-body review-comment-body'>{ - Object.entries(comments) - .map( - ([key, thread]) => - <div className='diff-container'> - <Diff key={key} hunks={thread[0].diffHunks} path={thread[0].path} /> - ...comments... - </div> - ) - }</div> + return <div className='comment-container comment'> + <div className='review-comment-container'> + <div className='review-comment-header'> + <Spaced> + <Avatar for={event.user} /> + <AuthorLink for={event.user} />{association(event)} + reviewed + <Timestamp href={event.htmlUrl} date={event.submittedAt} /> + </Spaced> </div> + <div className='comment-body review-comment-body'>{ + Object.entries(comments) + .map( + ([key, thread]) => + <div className='diff-container'> + <Diff key={key} + hunks={thread[0].diffHunks} + outdated={thread[0].position === null} + path={thread[0].path} /> + {thread.map(c => <CommentView {...c} />)} + </div> + ) + }</div> </div> - </>; + </div>; }; -const Diff = ({ hunks, path }: { hunks: DiffHunk[], path: string }) => +const Diff = ({ hunks, path, outdated=false }: { hunks: DiffHunk[], outdated: boolean, path: string }) => <div className='diff'> <div className='diffHeader'> - <span className='diffPath'>{path}</span> + <span className={`diffPath ${outdated ? 'outdated' : ''}`}>{path}</span> </div> {hunks.map(hunk => <Hunk hunk={hunk} />)} </div>; @@ -208,7 +221,6 @@ const LineNumber = ({ num }: { num: number }) => // </div> // } - const CommentEventView = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; const MergedEventView = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; const AssignEventView = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; @@ -231,4 +243,19 @@ export function getDiffChangeType(text: string) { } const getDiffChangeClass = (type: DiffChangeType) => - DiffChangeType[type].toLowerCase(); \ No newline at end of file + DiffChangeType[type].toLowerCase(); + +const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Comment) => + <div className='comment-container comment review-comment'> + <div className='review-comment-container'> + <div className='review-comment-header'> + <Spaced> + <Avatar for={user} /> + <AuthorLink for={user} /> + commented + <Timestamp href={htmlUrl} date={createdAt} /> + </Spaced> + </div> + <CommentBody bodyHTML={bodyHTML} body={body} /> + </div> + </div>; \ No newline at end of file From 2880fb075dd17b1d30df360965a1810eb69ead5d Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 29 Mar 2019 20:25:56 +0800 Subject: [PATCH 07/50] Pull request mutation context. --- preview-src/actions.tsx | 72 ++++ preview-src/app.tsx | 17 +- preview-src/index.ts | 792 ++++++++++++++++++++-------------------- preview-src/views.tsx | 26 +- 4 files changed, 503 insertions(+), 404 deletions(-) create mode 100644 preview-src/actions.tsx diff --git a/preview-src/actions.tsx b/preview-src/actions.tsx new file mode 100644 index 0000000000..9a6f198262 --- /dev/null +++ b/preview-src/actions.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; + +import { createContext, useState, useContext, useEffect } from 'react'; +import { getMessageHandler, MessageHandler } from './message'; +import { PullRequest } from './cache'; + +export class PRContext { + constructor( + public pr: PullRequest = null, + public onchange: ((ctx: PullRequest) => void) | null = null, + private _handler: MessageHandler = null) { + if (!_handler) { + console.log('init message handler', this.handleMessage); + this._handler = getMessageHandler(this.handleMessage); + } + } + + public checkout(this: PRContext): Promise<void> { + return this.postMessage({ command: 'pr.checkout' }); + } + + public async exitReviewMode(this: PRContext): Promise<void> { + console.log('Exit', this.pr, this.pr && this.pr.repositoryDefaultBranch, this.postMessage); + if (!this.pr) { return; } + console.log(this.pr.repositoryDefaultBranch); + return this.postMessage({ + command: 'pr.checkout-default-branch', + args: this.pr.repositoryDefaultBranch, + }); + } + + public refresh() { + return this.postMessage({ command: 'pr.refresh' }); + } + + setPR(pr: PullRequest) { + this.pr = pr; + if (this.onchange) { this.onchange(this.pr); } + return this; + } + + updatePR(pr: Partial<PullRequest>) { + this.pr = {...this.pr, ...pr } + if (this.onchange) { this.onchange(this.pr); } + return this; + } + + private postMessage(message: any) { + return this._handler.postMessage(message); + } + + handleMessage = (message: any) => { + console.log('message from host:', message); + switch (message.command) { + case 'pr.initialize': + return this.setPR(message.pullrequest); + case 'update-state': + return this.updatePR({ state: message.state }); + case 'pr.update-checkout-status': + return this.updatePR({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); + case 'pr.enable-exit': + return this.updatePR({ isCurrentlyCheckedOut: true }); + case 'set-scroll': + window.scrollTo(message.scrollPosition.x, message.scrollPosition.y); + } + } + + public static instance = new PRContext(); +} + +const Context = createContext<PRContext>(PRContext.instance); +export default Context; diff --git a/preview-src/app.tsx b/preview-src/app.tsx index 8c0bdba77a..f7dda1b72a 100644 --- a/preview-src/app.tsx +++ b/preview-src/app.tsx @@ -1,8 +1,21 @@ import * as React from 'react'; +import { useContext, useState, useEffect } from 'react'; import { render } from 'react-dom'; import { Overview } from './views'; +import PRContext from './actions'; import { PullRequest } from './cache'; -export function main(pr: PullRequest) { - render(<Overview {...pr} />, document.getElementById('main')); +export function main() { + render( + <Root>{pr => <Overview {...pr} />}</Root> + , document.getElementById('main')); } + +function Root({ children }) { + const ctx = useContext(PRContext); + const [pr, setPR] = useState<PullRequest>(ctx.pr); + useEffect(() => { + ctx.onchange = setPR; + }, []); + return pr ? children(pr) : 'Loading...'; +} \ No newline at end of file diff --git a/preview-src/index.ts b/preview-src/index.ts index d8c5208cf5..ff226a2f72 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -3,403 +3,405 @@ * 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 { renderTimelineEvent, getStatus, renderComment, renderReview, ActionsBar, renderStatusChecks, updatePullRequestState, ElementIds } from './pullRequestOverviewRenderer'; -import md from './mdRenderer'; -const emoji = require('node-emoji'); -import { getMessageHandler } from './message'; -import { getState, setState, PullRequest, updateState } from './cache'; +// 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 { renderTimelineEvent, getStatus, renderComment, renderReview, ActionsBar, renderStatusChecks, updatePullRequestState, ElementIds } from './pullRequestOverviewRenderer'; +// import md from './mdRenderer'; +// const emoji = require('node-emoji'); +// import { getMessageHandler } from './message'; +// import { getState, setState, PullRequest, updateState } from './cache'; import { main } from './app'; +console.log('hi'); +main(); + window.onload = () => { - const pullRequest = getState(); - if (pullRequest && Object.keys(pullRequest).length) { - renderPullRequest(pullRequest); - } + // 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); - updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); - renderPullRequest(getState()); - break; - case 'pr.enable-exit': - (<HTMLButtonElement>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 { - main(pr); - renderTimelineEvents(pr); - setTitleHTML(pr); - setTextArea(); - renderStatusChecks(pr, messageHandler); - updateCheckoutButton(pr.isCurrentlyCheckedOut); - updatePullRequestState(pr.state); - - addEventListeners(pr); -} - -function renderTimelineEvents(pr: PullRequest): void { - const timelineElement = document.getElementById(ElementIds.TimelineEvents)!; - timelineElement.innerHTML = ''; - pr.events - .map(event => renderTimelineEvent(event, messageHandler, pr)) - .filter(event => event !== undefined) - .forEach(renderedEvent => timelineElement.appendChild(renderedEvent as HTMLElement)); -} - -function setTitleHTML(pr: PullRequest): void { - document.getElementById('title')!.innerHTML = ` - <div id="details" class="details"> - <div id="overview-title" class="overview-title"> - <div class="button-group"> - <button id="${ElementIds.Checkout}" aria-live="polite"></button> - <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> - <button id="${ElementIds.Refresh}">Refresh</button> - </div> - </div> - <div class="subtitle"> - <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> - <img class="avatar" src="${pr.author.avatarUrl}" alt=""> - <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> - <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> - </div> - </div> - `; - - const title = renderTitle(pr); - (document.getElementById('overview-title')! as any).prepend(title); - - const description = renderDescription(pr); - document.getElementById('details')!.appendChild(description); -} - -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 = `(<a href=${pr.url}>#${pr.number}</a>)`; - - 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): HTMLElement { - const commentContainer = document.createElement('div'); - commentContainer.classList.add('description-container'); - - const commentHeader = document.createElement('div'); - commentHeader.classList.add('description-header'); - - const commentBody = document.createElement('div'); - commentBody.className = 'comment-body'; - commentBody.innerHTML = pr.bodyHTML ? - pr.bodyHTML : - pr.body - ? md.render(emoji.emojify(pr.body)) - : '<p><i>No description provided.</i></p>'; - - if (pr.labels.length) { - const line = document.createElement('div'); - line.classList.add('line'); - - line.innerHTML = `<svg class="octicon octicon-tag" viewBox="0 0 14 16" version="1.1" width="14" height="16"> - <path fill-rule="evenodd" d="M7.685 1.72a2.49 2.49 0 0 0-1.76-.726H3.48A2.5 2.5 0 0 0 .994 3.48v2.456c0 .656.269 1.292.726 1.76l6.024 6.024a.99.99 0 0 0 1.402 0l4.563-4.563a.99.99 0 0 0 0-1.402L7.685 1.72zM2.366 7.048a1.54 1.54 0 0 1-.467-1.123V3.48c0-.874.716-1.58 1.58-1.58h2.456c.418 0 .825.159 1.123.467l6.104 6.094-4.702 4.702-6.094-6.114zm.626-4.066h1.989v1.989H2.982V2.982h.01z" /> - </svg> - ${pr.labels.map(label => `<span class="label">${label}</span>`).join('')}`; - - commentContainer.appendChild(line); - } - - commentContainer.appendChild(commentHeader); - commentContainer.appendChild(commentBody); - - if (pr.canEdit) { - function updateDescription(text: string) { - pr.body = text; - updateState({ body: text }); - - if (!text) { - commentBody.innerHTML = `<p><i>No description provided.</i></p>`; - } - } - - const actionsBar = new ActionsBar(commentContainer, { body: pr.body, id: pr.number.toString() }, commentBody, messageHandler, updateDescription, 'pr.edit-description'); - const renderedActionsBar = actionsBar.render(); - actionsBar.registerActionBarListeners(); - commentHeader.appendChild(renderedActionsBar); - - if (pr.pendingCommentDrafts && pr.pendingCommentDrafts[pr.number]) { - actionsBar.startEdit(pr.pendingCommentDrafts[pr.number]); - } - } - - return commentContainer; -} - -function addEventListeners(pr: PullRequest): void { - document.getElementById(ElementIds.Checkout)!.addEventListener('click', async () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; - (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; - const { state } = getState(); - (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; - (<HTMLButtonElement>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 () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; - const inputBox = (<HTMLTextAreaElement>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 () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; - const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.approve', - args: inputBox.value - }).then(message => { - // succeed - appendReview(message.value); - }, err => { - // enable approve button - (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; - }); - }); - } - - const requestChangesButton = document.getElementById(ElementIds.RequestChanges); - if (requestChangesButton) { - requestChangesButton.addEventListener('click', () => { - (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; - const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.request-changes', - args: inputBox.value - }).then(message => { - appendReview(message.value); - }, err => { - (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; - }); - }); - } - - document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { - (<HTMLButtonElement>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); -} - -function clearTextArea() { - (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value = ''; - (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; - (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; - - updateState({ pendingCommentText: undefined }); -} - -async function submitComment() { - (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; - const result = await messageHandler.postMessage({ - command: 'pr.comment', - args: (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value - }); - - appendComment(result.value); -} - -function appendReview(review: any): void { - review.event = EventType.Reviewed; - const pullRequest = getState(); - let events = pullRequest.events; - events.push(review); - updateState({ events: events }); - - const newReview = renderReview(review, messageHandler, pullRequest.supportsGraphQl); - if (newReview) { - document.getElementById(ElementIds.TimelineEvents)!.appendChild(newReview); - } - clearTextArea(); -} - -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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); - const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); - checkoutButton.disabled = isCheckedOut; - checkoutMasterButton.disabled = false; - const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; - checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; - - const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> - <div class="form-actions"> - <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> - ${ displaySubmitButtonsOnPendingReview - ? '' - : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> - <button id="${ElementIds.Approve}" class="secondary">Approve</button>` - } - <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> - </div>`; - - const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); - textArea.placeholder = 'Leave a comment'; - textArea.addEventListener('keydown', e => { - if (e.keyCode === 65 && e.metaKey) { - (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; - replyButton.disabled = false; - - const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; - requestChangesButton.disabled = false; - } -} +// 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); +// updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); +// renderPullRequest(getState()); +// break; +// case 'pr.enable-exit': +// (<HTMLButtonElement>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); +// setTitleHTML(pr); +// setTextArea(); +// renderStatusChecks(pr, messageHandler); +// updateCheckoutButton(pr.isCurrentlyCheckedOut); +// updatePullRequestState(pr.state); + +// addEventListeners(pr); +// } + +// function renderTimelineEvents(pr: PullRequest): void { +// const timelineElement = document.getElementById(ElementIds.TimelineEvents)!; +// timelineElement.innerHTML = ''; +// pr.events +// .map(event => renderTimelineEvent(event, messageHandler, pr)) +// .filter(event => event !== undefined) +// .forEach(renderedEvent => timelineElement.appendChild(renderedEvent as HTMLElement)); +// } + +// function setTitleHTML(pr: PullRequest): void { +// document.getElementById('title')!.innerHTML = ` +// <div id="details" class="details"> +// <div id="overview-title" class="overview-title"> +// <div class="button-group"> +// <button id="${ElementIds.Checkout}" aria-live="polite"></button> +// <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> +// <button id="${ElementIds.Refresh}">Refresh</button> +// </div> +// </div> +// <div class="subtitle"> +// <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> +// <img class="avatar" src="${pr.author.avatarUrl}" alt=""> +// <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> +// <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> +// </div> +// </div> +// `; + +// const title = renderTitle(pr); +// (document.getElementById('overview-title')! as any).prepend(title); + +// const description = renderDescription(pr); +// document.getElementById('details')!.appendChild(description); +// } + +// 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 = `(<a href=${pr.url}>#${pr.number}</a>)`; + +// 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): HTMLElement { +// const commentContainer = document.createElement('div'); +// commentContainer.classList.add('description-container'); + +// const commentHeader = document.createElement('div'); +// commentHeader.classList.add('description-header'); + +// const commentBody = document.createElement('div'); +// commentBody.className = 'comment-body'; +// commentBody.innerHTML = pr.bodyHTML ? +// pr.bodyHTML : +// pr.body +// ? md.render(emoji.emojify(pr.body)) +// : '<p><i>No description provided.</i></p>'; + +// if (pr.labels.length) { +// const line = document.createElement('div'); +// line.classList.add('line'); + +// line.innerHTML = `<svg class="octicon octicon-tag" viewBox="0 0 14 16" version="1.1" width="14" height="16"> +// <path fill-rule="evenodd" d="M7.685 1.72a2.49 2.49 0 0 0-1.76-.726H3.48A2.5 2.5 0 0 0 .994 3.48v2.456c0 .656.269 1.292.726 1.76l6.024 6.024a.99.99 0 0 0 1.402 0l4.563-4.563a.99.99 0 0 0 0-1.402L7.685 1.72zM2.366 7.048a1.54 1.54 0 0 1-.467-1.123V3.48c0-.874.716-1.58 1.58-1.58h2.456c.418 0 .825.159 1.123.467l6.104 6.094-4.702 4.702-6.094-6.114zm.626-4.066h1.989v1.989H2.982V2.982h.01z" /> +// </svg> +// ${pr.labels.map(label => `<span class="label">${label}</span>`).join('')}`; + +// commentContainer.appendChild(line); +// } + +// commentContainer.appendChild(commentHeader); +// commentContainer.appendChild(commentBody); + +// if (pr.canEdit) { +// function updateDescription(text: string) { +// pr.body = text; +// updateState({ body: text }); + +// if (!text) { +// commentBody.innerHTML = `<p><i>No description provided.</i></p>`; +// } +// } + +// const actionsBar = new ActionsBar(commentContainer, { body: pr.body, id: pr.number.toString() }, commentBody, messageHandler, updateDescription, 'pr.edit-description'); +// const renderedActionsBar = actionsBar.render(); +// actionsBar.registerActionBarListeners(); +// commentHeader.appendChild(renderedActionsBar); + +// if (pr.pendingCommentDrafts && pr.pendingCommentDrafts[pr.number]) { +// actionsBar.startEdit(pr.pendingCommentDrafts[pr.number]); +// } +// } + +// return commentContainer; +// } + +// function addEventListeners(pr: PullRequest): void { +// document.getElementById(ElementIds.Checkout)!.addEventListener('click', async () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; +// (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; +// const { state } = getState(); +// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; +// (<HTMLButtonElement>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 () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>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 () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); +// messageHandler.postMessage({ +// command: 'pr.approve', +// args: inputBox.value +// }).then(message => { +// // succeed +// appendReview(message.value); +// }, err => { +// // enable approve button +// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; +// }); +// }); +// } + +// const requestChangesButton = document.getElementById(ElementIds.RequestChanges); +// if (requestChangesButton) { +// requestChangesButton.addEventListener('click', () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); +// messageHandler.postMessage({ +// command: 'pr.request-changes', +// args: inputBox.value +// }).then(message => { +// appendReview(message.value); +// }, err => { +// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; +// }); +// }); +// } + +// document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { +// (<HTMLButtonElement>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); +// } + +// function clearTextArea() { +// (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value = ''; +// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; +// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; + +// updateState({ pendingCommentText: undefined }); +// } + +// async function submitComment() { +// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; +// const result = await messageHandler.postMessage({ +// command: 'pr.comment', +// args: (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value +// }); + +// appendComment(result.value); +// } + +// function appendReview(review: any): void { +// review.event = EventType.Reviewed; +// const pullRequest = getState(); +// let events = pullRequest.events; +// events.push(review); +// updateState({ events: events }); + +// const newReview = renderReview(review, messageHandler, pullRequest.supportsGraphQl); +// if (newReview) { +// document.getElementById(ElementIds.TimelineEvents)!.appendChild(newReview); +// } +// clearTextArea(); +// } + +// 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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); +// const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); +// checkoutButton.disabled = isCheckedOut; +// checkoutMasterButton.disabled = false; +// const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; +// checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; + +// const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> +// <div class="form-actions"> +// <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> +// ${ displaySubmitButtonsOnPendingReview +// ? '' +// : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> +// <button id="${ElementIds.Approve}" class="secondary">Approve</button>` +// } +// <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> +// </div>`; + +// const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); +// textArea.placeholder = 'Leave a comment'; +// textArea.addEventListener('keydown', e => { +// if (e.keyCode === 65 && e.metaKey) { +// (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; +// replyButton.disabled = false; + +// const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; +// requestChangesButton.disabled = false; +// } +// } diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 3a76aec167..c0060661fe 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -4,6 +4,7 @@ import { Comment } from '../src/common/comment'; import { getStatus } from './pullRequestOverviewRenderer'; import { PullRequest } from './cache'; import md from './mdRenderer'; +import Context from './actions'; export const Overview = (pr: PullRequest) => <> @@ -20,7 +21,7 @@ const Avatar = ({ for: author }: { for: PullRequest['author'] }) => const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => <a href={author.url}>{text}</a>; -const nbsp = String.fromCharCode(0xa0) +const nbsp = String.fromCharCode(0xa0); const Spaced = ({ children }) => { const count = React.Children.count(children); return React.createElement(React.Fragment, { @@ -43,11 +44,7 @@ export const Header = ({ state, title, head, base, url, createdAt, author, isCur <div className='overview-title'> <h2>{title}</h2> <div className='button-group'> - { - isCurrentlyCheckedOut - ? <button aria-live='polite'>Exit Review Mode</button> - : <button aria-live='polite'>Checkout</button> - } + <CheckoutButtons /> <button>Refresh</button> </div> </div> @@ -69,6 +66,20 @@ export const Header = ({ state, title, head, base, url, createdAt, author, isCur </div> </>; +const CheckoutButtons = () => { + const pr = useContext(Context); + console.log('pr=', pr); + if (!pr) { return; } + if (pr.pr.isCurrentlyCheckedOut) { + return <> + <button aria-live='polite' className='checkedOut' disabled>Checked Out</button> + <button aria-live='polite' onClick={() => pr.exitReviewMode()}>Exit Review Mode</button> + </>; + } else { + return <button aria-live='polite' onClick={() => pr.checkout()}>Checkout</button>; + } +}; + const Timestamp = ({ date, href, @@ -142,7 +153,7 @@ const CommitEventView = (event: CommitEvent) => <AuthorLink for={event.author} /> <div className='message'>{event.message}</div> </div> - <a className='sha' href={event.url}>{event.sha}</a> + <a className='sha' href={event.url}>{event.sha.slice(0, 7)}</a> </div>; const association = ({ authorAssociation }: ReviewEvent, @@ -153,6 +164,7 @@ const association = ({ authorAssociation }: ReviewEvent, import { groupBy } from 'lodash'; import { DiffHunk, DiffLine } from '../src/common/diffHunk'; +import { useContext } from 'react'; const positionKey = (comment: Comment) => comment.position !== null From f8e408ba1c9c1d011e2ed530d19c836993271c4b Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 29 Mar 2019 21:29:58 +0800 Subject: [PATCH 08/50] Preserve webview state in cache. --- preview-src/actions.tsx | 11 +++++------ preview-src/cache.ts | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/preview-src/actions.tsx b/preview-src/actions.tsx index 9a6f198262..a3489512be 100644 --- a/preview-src/actions.tsx +++ b/preview-src/actions.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; -import { createContext, useState, useContext, useEffect } from 'react'; +import { createContext } from 'react'; import { getMessageHandler, MessageHandler } from './message'; -import { PullRequest } from './cache'; +import { PullRequest, getState, setState } from './cache'; export class PRContext { constructor( - public pr: PullRequest = null, + public pr: PullRequest = getState(), public onchange: ((ctx: PullRequest) => void) | null = null, private _handler: MessageHandler = null) { if (!_handler) { @@ -35,14 +35,13 @@ export class PRContext { setPR(pr: PullRequest) { this.pr = pr; + setState(this.pr); if (this.onchange) { this.onchange(this.pr); } return this; } updatePR(pr: Partial<PullRequest>) { - this.pr = {...this.pr, ...pr } - if (this.onchange) { this.onchange(this.pr); } - return this; + return this.setPR({...this.pr, ...pr }); } private postMessage(message: any) { diff --git a/preview-src/cache.ts b/preview-src/cache.ts index 40a4d72dbc..1a0894a9e9 100644 --- a/preview-src/cache.ts +++ b/preview-src/cache.ts @@ -34,19 +34,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<PullRequest>): void { From 500a851fcac4cf26a5c543202c31fc5cde07827c Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Sat, 30 Mar 2019 10:28:31 +0800 Subject: [PATCH 09/50] WIP: Comments and status checks. --- preview-src/actions.tsx | 4 - preview-src/index.ts | 770 ++++++++++++++++++++-------------------- 2 files changed, 385 insertions(+), 389 deletions(-) diff --git a/preview-src/actions.tsx b/preview-src/actions.tsx index a3489512be..e846733f66 100644 --- a/preview-src/actions.tsx +++ b/preview-src/actions.tsx @@ -1,5 +1,3 @@ -import * as React from 'react'; - import { createContext } from 'react'; import { getMessageHandler, MessageHandler } from './message'; import { PullRequest, getState, setState } from './cache'; @@ -20,9 +18,7 @@ export class PRContext { } public async exitReviewMode(this: PRContext): Promise<void> { - console.log('Exit', this.pr, this.pr && this.pr.repositoryDefaultBranch, this.postMessage); if (!this.pr) { return; } - console.log(this.pr.repositoryDefaultBranch); return this.postMessage({ command: 'pr.checkout-default-branch', args: this.pr.repositoryDefaultBranch, diff --git a/preview-src/index.ts b/preview-src/index.ts index ff226a2f72..ef116f7d95 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -18,390 +18,390 @@ console.log('hi'); main(); window.onload = () => { - // const pullRequest = getState(); - // if (pullRequest && Object.keys(pullRequest).length) { - // renderPullRequest(pullRequest); - // } + 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); -// updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); -// renderPullRequest(getState()); -// break; -// case 'pr.enable-exit': -// (<HTMLButtonElement>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); -// setTitleHTML(pr); -// setTextArea(); -// renderStatusChecks(pr, messageHandler); -// updateCheckoutButton(pr.isCurrentlyCheckedOut); -// updatePullRequestState(pr.state); - -// addEventListeners(pr); -// } - -// function renderTimelineEvents(pr: PullRequest): void { -// const timelineElement = document.getElementById(ElementIds.TimelineEvents)!; -// timelineElement.innerHTML = ''; -// pr.events -// .map(event => renderTimelineEvent(event, messageHandler, pr)) -// .filter(event => event !== undefined) -// .forEach(renderedEvent => timelineElement.appendChild(renderedEvent as HTMLElement)); -// } - -// function setTitleHTML(pr: PullRequest): void { -// document.getElementById('title')!.innerHTML = ` -// <div id="details" class="details"> -// <div id="overview-title" class="overview-title"> -// <div class="button-group"> -// <button id="${ElementIds.Checkout}" aria-live="polite"></button> -// <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> -// <button id="${ElementIds.Refresh}">Refresh</button> -// </div> -// </div> -// <div class="subtitle"> -// <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> -// <img class="avatar" src="${pr.author.avatarUrl}" alt=""> -// <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> -// <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> -// </div> -// </div> -// `; - -// const title = renderTitle(pr); -// (document.getElementById('overview-title')! as any).prepend(title); - -// const description = renderDescription(pr); -// document.getElementById('details')!.appendChild(description); -// } - -// 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 = `(<a href=${pr.url}>#${pr.number}</a>)`; - -// 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): HTMLElement { -// const commentContainer = document.createElement('div'); -// commentContainer.classList.add('description-container'); - -// const commentHeader = document.createElement('div'); -// commentHeader.classList.add('description-header'); - -// const commentBody = document.createElement('div'); -// commentBody.className = 'comment-body'; -// commentBody.innerHTML = pr.bodyHTML ? -// pr.bodyHTML : -// pr.body -// ? md.render(emoji.emojify(pr.body)) -// : '<p><i>No description provided.</i></p>'; - -// if (pr.labels.length) { -// const line = document.createElement('div'); -// line.classList.add('line'); - -// line.innerHTML = `<svg class="octicon octicon-tag" viewBox="0 0 14 16" version="1.1" width="14" height="16"> -// <path fill-rule="evenodd" d="M7.685 1.72a2.49 2.49 0 0 0-1.76-.726H3.48A2.5 2.5 0 0 0 .994 3.48v2.456c0 .656.269 1.292.726 1.76l6.024 6.024a.99.99 0 0 0 1.402 0l4.563-4.563a.99.99 0 0 0 0-1.402L7.685 1.72zM2.366 7.048a1.54 1.54 0 0 1-.467-1.123V3.48c0-.874.716-1.58 1.58-1.58h2.456c.418 0 .825.159 1.123.467l6.104 6.094-4.702 4.702-6.094-6.114zm.626-4.066h1.989v1.989H2.982V2.982h.01z" /> -// </svg> -// ${pr.labels.map(label => `<span class="label">${label}</span>`).join('')}`; - -// commentContainer.appendChild(line); -// } - -// commentContainer.appendChild(commentHeader); -// commentContainer.appendChild(commentBody); - -// if (pr.canEdit) { -// function updateDescription(text: string) { -// pr.body = text; -// updateState({ body: text }); - -// if (!text) { -// commentBody.innerHTML = `<p><i>No description provided.</i></p>`; -// } -// } - -// const actionsBar = new ActionsBar(commentContainer, { body: pr.body, id: pr.number.toString() }, commentBody, messageHandler, updateDescription, 'pr.edit-description'); -// const renderedActionsBar = actionsBar.render(); -// actionsBar.registerActionBarListeners(); -// commentHeader.appendChild(renderedActionsBar); - -// if (pr.pendingCommentDrafts && pr.pendingCommentDrafts[pr.number]) { -// actionsBar.startEdit(pr.pendingCommentDrafts[pr.number]); -// } -// } - -// return commentContainer; -// } - -// function addEventListeners(pr: PullRequest): void { -// document.getElementById(ElementIds.Checkout)!.addEventListener('click', async () => { -// (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; -// (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; -// const { state } = getState(); -// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; -// (<HTMLButtonElement>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 () => { -// (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; -// const inputBox = (<HTMLTextAreaElement>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 () => { -// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; -// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); -// messageHandler.postMessage({ -// command: 'pr.approve', -// args: inputBox.value -// }).then(message => { -// // succeed -// appendReview(message.value); -// }, err => { -// // enable approve button -// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; -// }); -// }); -// } - -// const requestChangesButton = document.getElementById(ElementIds.RequestChanges); -// if (requestChangesButton) { -// requestChangesButton.addEventListener('click', () => { -// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; -// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); -// messageHandler.postMessage({ -// command: 'pr.request-changes', -// args: inputBox.value -// }).then(message => { -// appendReview(message.value); -// }, err => { -// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; -// }); -// }); -// } - -// document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { -// (<HTMLButtonElement>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); -// } - -// function clearTextArea() { -// (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value = ''; -// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; -// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; - -// updateState({ pendingCommentText: undefined }); -// } - -// async function submitComment() { -// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; -// const result = await messageHandler.postMessage({ -// command: 'pr.comment', -// args: (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value -// }); - -// appendComment(result.value); -// } - -// function appendReview(review: any): void { -// review.event = EventType.Reviewed; -// const pullRequest = getState(); -// let events = pullRequest.events; -// events.push(review); -// updateState({ events: events }); - -// const newReview = renderReview(review, messageHandler, pullRequest.supportsGraphQl); -// if (newReview) { -// document.getElementById(ElementIds.TimelineEvents)!.appendChild(newReview); -// } -// clearTextArea(); -// } - -// 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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); -// const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); -// checkoutButton.disabled = isCheckedOut; -// checkoutMasterButton.disabled = false; -// const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; -// checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; - -// const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> -// <div class="form-actions"> -// <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> -// ${ displaySubmitButtonsOnPendingReview -// ? '' -// : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> -// <button id="${ElementIds.Approve}" class="secondary">Approve</button>` -// } -// <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> -// </div>`; - -// const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); -// textArea.placeholder = 'Leave a comment'; -// textArea.addEventListener('keydown', e => { -// if (e.keyCode === 65 && e.metaKey) { -// (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; -// replyButton.disabled = false; - -// const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; -// requestChangesButton.disabled = false; -// } -// } +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); + updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); + renderPullRequest(getState()); + break; + case 'pr.enable-exit': + (<HTMLButtonElement>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); + setTitleHTML(pr); + setTextArea(); + renderStatusChecks(pr, messageHandler); + updateCheckoutButton(pr.isCurrentlyCheckedOut); + updatePullRequestState(pr.state); + + addEventListeners(pr); +} + +function renderTimelineEvents(pr: PullRequest): void { + const timelineElement = document.getElementById(ElementIds.TimelineEvents)!; + timelineElement.innerHTML = ''; + pr.events + .map(event => renderTimelineEvent(event, messageHandler, pr)) + .filter(event => event !== undefined) + .forEach(renderedEvent => timelineElement.appendChild(renderedEvent as HTMLElement)); +} + +function setTitleHTML(pr: PullRequest): void { + document.getElementById('title')!.innerHTML = ` + <div id="details" class="details"> + <div id="overview-title" class="overview-title"> + <div class="button-group"> + <button id="${ElementIds.Checkout}" aria-live="polite"></button> + <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> + <button id="${ElementIds.Refresh}">Refresh</button> + </div> + </div> + <div class="subtitle"> + <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> + <img class="avatar" src="${pr.author.avatarUrl}" alt=""> + <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> + <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> + </div> + </div> + `; + + const title = renderTitle(pr); + (document.getElementById('overview-title')! as any).prepend(title); + + const description = renderDescription(pr); + document.getElementById('details')!.appendChild(description); +} + +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 = `(<a href=${pr.url}>#${pr.number}</a>)`; + + 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): HTMLElement { + const commentContainer = document.createElement('div'); + commentContainer.classList.add('description-container'); + + const commentHeader = document.createElement('div'); + commentHeader.classList.add('description-header'); + + const commentBody = document.createElement('div'); + commentBody.className = 'comment-body'; + commentBody.innerHTML = pr.bodyHTML ? + pr.bodyHTML : + pr.body + ? md.render(emoji.emojify(pr.body)) + : '<p><i>No description provided.</i></p>'; + + if (pr.labels.length) { + const line = document.createElement('div'); + line.classList.add('line'); + + line.innerHTML = `<svg class="octicon octicon-tag" viewBox="0 0 14 16" version="1.1" width="14" height="16"> + <path fill-rule="evenodd" d="M7.685 1.72a2.49 2.49 0 0 0-1.76-.726H3.48A2.5 2.5 0 0 0 .994 3.48v2.456c0 .656.269 1.292.726 1.76l6.024 6.024a.99.99 0 0 0 1.402 0l4.563-4.563a.99.99 0 0 0 0-1.402L7.685 1.72zM2.366 7.048a1.54 1.54 0 0 1-.467-1.123V3.48c0-.874.716-1.58 1.58-1.58h2.456c.418 0 .825.159 1.123.467l6.104 6.094-4.702 4.702-6.094-6.114zm.626-4.066h1.989v1.989H2.982V2.982h.01z" /> + </svg> + ${pr.labels.map(label => `<span class="label">${label}</span>`).join('')}`; + + commentContainer.appendChild(line); + } + + commentContainer.appendChild(commentHeader); + commentContainer.appendChild(commentBody); + + if (pr.canEdit) { + function updateDescription(text: string) { + pr.body = text; + updateState({ body: text }); + + if (!text) { + commentBody.innerHTML = `<p><i>No description provided.</i></p>`; + } + } + + const actionsBar = new ActionsBar(commentContainer, { body: pr.body, id: pr.number.toString() }, commentBody, messageHandler, updateDescription, 'pr.edit-description'); + const renderedActionsBar = actionsBar.render(); + actionsBar.registerActionBarListeners(); + commentHeader.appendChild(renderedActionsBar); + + if (pr.pendingCommentDrafts && pr.pendingCommentDrafts[pr.number]) { + actionsBar.startEdit(pr.pendingCommentDrafts[pr.number]); + } + } + + return commentContainer; +} + +function addEventListeners(pr: PullRequest): void { + document.getElementById(ElementIds.Checkout)!.addEventListener('click', async () => { + (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; + (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; + const { state } = getState(); + (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; + (<HTMLButtonElement>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 () => { + (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; + const inputBox = (<HTMLTextAreaElement>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 () => { + (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; + const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); + messageHandler.postMessage({ + command: 'pr.approve', + args: inputBox.value + }).then(message => { + // succeed + appendReview(message.value); + }, err => { + // enable approve button + (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; + }); + }); + } + + const requestChangesButton = document.getElementById(ElementIds.RequestChanges); + if (requestChangesButton) { + requestChangesButton.addEventListener('click', () => { + (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; + const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); + messageHandler.postMessage({ + command: 'pr.request-changes', + args: inputBox.value + }).then(message => { + appendReview(message.value); + }, err => { + (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; + }); + }); + } + + document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { + (<HTMLButtonElement>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); +} + +function clearTextArea() { + (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value = ''; + (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; + (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; + + updateState({ pendingCommentText: undefined }); +} + +async function submitComment() { + (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; + const result = await messageHandler.postMessage({ + command: 'pr.comment', + args: (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!).value + }); + + appendComment(result.value); +} + +function appendReview(review: any): void { + review.event = EventType.Reviewed; + const pullRequest = getState(); + let events = pullRequest.events; + events.push(review); + updateState({ events: events }); + + const newReview = renderReview(review, messageHandler, pullRequest.supportsGraphQl); + if (newReview) { + document.getElementById(ElementIds.TimelineEvents)!.appendChild(newReview); + } + clearTextArea(); +} + +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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); + const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); + checkoutButton.disabled = isCheckedOut; + checkoutMasterButton.disabled = false; + const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; + checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; + + const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> + <div class="form-actions"> + <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> + ${ displaySubmitButtonsOnPendingReview + ? '' + : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> + <button id="${ElementIds.Approve}" class="secondary">Approve</button>` + } + <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> + </div>`; + + const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); + textArea.placeholder = 'Leave a comment'; + textArea.addEventListener('keydown', e => { + if (e.keyCode === 65 && e.metaKey) { + (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; + replyButton.disabled = false; + + const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; + requestChangesButton.disabled = false; + } +} From 5383098091e2576409750a32bda9e4d514860efc Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Sat, 30 Mar 2019 11:12:16 +0800 Subject: [PATCH 10/50] Status check text and icons. --- preview-src/index.ts | 672 +++++++++++++++--------------- preview-src/views.tsx | 86 +++- src/github/pullRequestOverview.ts | 2 +- 3 files changed, 413 insertions(+), 347 deletions(-) diff --git a/preview-src/index.ts b/preview-src/index.ts index c102532b8d..1d40e373b8 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -3,344 +3,344 @@ * 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 * 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'; +// import { getMessageHandler } from './message'; +// import { getState, setState, PullRequest, updateState } from './cache'; import { main } from './app'; main(); -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); - updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); - renderPullRequest(getState()); - break; - case 'pr.enable-exit': - (<HTMLButtonElement>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 = ` - <div id="details" class="details"> - <div id="overview-title" class="overview-title"> - <div class="button-group"> - <button id="${ElementIds.Checkout}" aria-live="polite"></button> - <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> - <button id="${ElementIds.Refresh}">Refresh</button> - </div> - </div> - <div class="subtitle"> - <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> - <img class="avatar" src="${pr.author.avatarUrl}" alt=""> - <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> - <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> - </div> - </div> - `; - - 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 = `(<a href=${pr.url}>#${pr.number}</a>)`; - - 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 ? '<i>No description provided</i>' : 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 () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; - (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; - const { state } = getState(); - (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; - (<HTMLButtonElement>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 () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; - const inputBox = (<HTMLTextAreaElement>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 () => { - (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; - const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.approve', - args: inputBox.value - }).then(message => { - // succeed - appendReview(message, messageHandler); - }, err => { - // enable approve button - (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; - }); - }); - } - - const requestChangesButton = document.getElementById(ElementIds.RequestChanges); - if (requestChangesButton) { - requestChangesButton.addEventListener('click', () => { - (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; - const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); - messageHandler.postMessage({ - command: 'pr.request-changes', - args: inputBox.value - }).then(message => { - appendReview(message, messageHandler); - }, err => { - (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; - }); - }); - } - - document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { - (<HTMLButtonElement>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() { - (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; - const result = await messageHandler.postMessage({ - command: 'pr.comment', - args: (<HTMLTextAreaElement>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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); - const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); - checkoutButton.disabled = isCheckedOut; - checkoutMasterButton.disabled = false; - const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; - checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; - - const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> - <div class="form-actions"> - <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> - ${ displaySubmitButtonsOnPendingReview - ? '' - : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> - <button id="${ElementIds.Approve}" class="secondary">Approve</button>` - } - <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> - </div>`; - - const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); - textArea.placeholder = 'Leave a comment'; - textArea.addEventListener('keydown', e => { - if (e.keyCode === 65 && e.metaKey) { - (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; - replyButton.disabled = false; - - const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; - requestChangesButton.disabled = false; - } -} +// 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); +// updateState({ isCurrentlyCheckedOut: message.isCurrentlyCheckedOut }); +// renderPullRequest(getState()); +// break; +// case 'pr.enable-exit': +// (<HTMLButtonElement>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 = ` +// <div id="details" class="details"> +// <div id="overview-title" class="overview-title"> +// <div class="button-group"> +// <button id="${ElementIds.Checkout}" aria-live="polite"></button> +// <button id="${ElementIds.CheckoutDefaultBranch}" aria-live="polite">Exit Review Mode</button> +// <button id="${ElementIds.Refresh}">Refresh</button> +// </div> +// </div> +// <div class="subtitle"> +// <div id="${ElementIds.Status}">${getStatus(pr.state)}</div> +// <img class="avatar" src="${pr.author.avatarUrl}" alt=""> +// <span class="author"><a href="${pr.author.url}">${pr.author.login}</a> wants to merge changes from <code>${pr.head}</code> to <code>${pr.base}</code>.</span> +// <span class="created-at">Created <a href=${pr.url} class="timestamp">${dateFromNow(pr.createdAt)}</a></span> +// </div> +// </div> +// `; + +// 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 = `(<a href=${pr.url}>#${pr.number}</a>)`; + +// 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 ? '<i>No description provided</i>' : 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 () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)).disabled = true; +// (<HTMLButtonElement>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 = (<HTMLInputElement>e.target).value; +// const { state } = getState(); +// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = !inputText; +// (<HTMLButtonElement>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 () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Close)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>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 () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); +// messageHandler.postMessage({ +// command: 'pr.approve', +// args: inputBox.value +// }).then(message => { +// // succeed +// appendReview(message, messageHandler); +// }, err => { +// // enable approve button +// (<HTMLButtonElement>document.getElementById(ElementIds.Approve)).disabled = false; +// }); +// }); +// } + +// const requestChangesButton = document.getElementById(ElementIds.RequestChanges); +// if (requestChangesButton) { +// requestChangesButton.addEventListener('click', () => { +// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = true; +// const inputBox = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)); +// messageHandler.postMessage({ +// command: 'pr.request-changes', +// args: inputBox.value +// }).then(message => { +// appendReview(message, messageHandler); +// }, err => { +// (<HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)).disabled = false; +// }); +// }); +// } + +// document.getElementById(ElementIds.CheckoutDefaultBranch)!.addEventListener('click', () => { +// (<HTMLButtonElement>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() { +// (<HTMLButtonElement>document.getElementById(ElementIds.Reply)).disabled = true; +// const result = await messageHandler.postMessage({ +// command: 'pr.comment', +// args: (<HTMLTextAreaElement>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 = (<HTMLButtonElement>document.getElementById(ElementIds.Checkout)); +// const checkoutMasterButton = (<HTMLButtonElement>document.getElementById(ElementIds.CheckoutDefaultBranch)); +// checkoutButton.disabled = isCheckedOut; +// checkoutMasterButton.disabled = false; +// const activeIcon = '<svg class="octicon octicon-check" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M12 5l-8 8-4-4 1.5-1.5L4 10l6.5-6.5L12 5z"></path></svg>'; +// checkoutButton.innerHTML = isCheckedOut ? `${activeIcon} Checked Out` : `Checkout`; + +// const backButton = (<HTMLButtonElement>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 = `<textarea id="${ElementIds.CommentTextArea}"></textarea> +// <div class="form-actions"> +// <button id="${ElementIds.Close}" class="secondary">Close Pull Request</button> +// ${ displaySubmitButtonsOnPendingReview +// ? '' +// : `<button id="${ElementIds.RequestChanges}" disabled="true" class="secondary">Request Changes</button> +// <button id="${ElementIds.Approve}" class="secondary">Approve</button>` +// } +// <button class="reply-button" id="${ElementIds.Reply}" disabled="true">Comment</button> +// </div>`; + +// const textArea = (<HTMLTextAreaElement>document.getElementById(ElementIds.CommentTextArea)!); +// textArea.placeholder = 'Leave a comment'; +// textArea.addEventListener('keydown', e => { +// if (e.keyCode === 65 && e.metaKey) { +// (<HTMLTextAreaElement>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 = <HTMLButtonElement>document.getElementById(ElementIds.Reply)!; +// replyButton.disabled = false; + +// const requestChangesButton = <HTMLButtonElement>document.getElementById(ElementIds.RequestChanges)!; +// requestChangesButton.disabled = false; +// } +// } diff --git a/preview-src/views.tsx b/preview-src/views.tsx index c0060661fe..14fc7e455c 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -6,10 +6,23 @@ import { PullRequest } from './cache'; import md from './mdRenderer'; import Context from './actions'; +const commitIconSvg = require('../resources/icons/commit_icon.svg'); +const mergeIconSvg = require('../resources/icons/merge_icon.svg'); +const editIcon = require('../resources/icons/edit.svg'); +const checkIcon = require('../resources/icons/check.svg'); + +const plusIcon = require('../resources/icons/plus.svg'); +const deleteIcon = require('../resources/icons/delete.svg'); + +const pendingIcon = require('../resources/icons/dot.svg'); +const commentIcon = require('../resources/icons/comment.svg'); +const diffIcon = require('../resources/icons/diff.svg'); + export const Overview = (pr: PullRequest) => <> <Details {...pr} /> <Timeline events={pr.events} /> + <StatusChecks {...pr} /> <hr/> </>; @@ -133,13 +146,6 @@ const Timeline = ({ events }: { events: TimelineEvent[] }) => ) }</>; -const commitIconSvg = require('../resources/icons/commit_icon.svg'); -// const mergeIconSvg = require('../resources/icons/merge_icon.svg'); -// const editIcon = require('../resources/icons/edit.svg'); -// const deleteIcon = require('../resources/icons/delete.svg'); -// const checkIcon = require('../resources/icons/check.svg'); -// const dotIcon = require('../resources/icons/dot.svg'); - const Icon = ({ src }: { src: string }) => <span dangerouslySetInnerHTML={{ __html: src }} />; @@ -165,6 +171,7 @@ const association = ({ authorAssociation }: ReviewEvent, import { groupBy } from 'lodash'; import { DiffHunk, DiffLine } from '../src/common/diffHunk'; import { useContext } from 'react'; +import { PullRequestStateEnum } from '../src/github/interface'; const positionKey = (comment: Comment) => comment.position !== null @@ -233,7 +240,7 @@ const LineNumber = ({ num }: { num: number }) => // </div> // } -const CommentEventView = (event: CommentEvent) => <h1>Comment: {event.id}</h1>; +const CommentEventView = (event: CommentEvent) => <CommentView {...event} />; const MergedEventView = (event: MergedEvent) => <h1>Merged: {event.id}</h1>; const AssignEventView = (event: AssignEvent) => <h1>Assign: {event.id}</h1>; @@ -257,7 +264,7 @@ export function getDiffChangeType(text: string) { const getDiffChangeClass = (type: DiffChangeType) => DiffChangeType[type].toLowerCase(); -const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Comment) => +const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Partial<Comment>) => <div className='comment-container comment review-comment'> <div className='review-comment-container'> <div className='review-comment-header'> @@ -270,4 +277,63 @@ const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Comment) => </div> <CommentBody bodyHTML={bodyHTML} body={body} /> </div> - </div>; \ No newline at end of file + </div>; + +const StatusChecks = ({ state, status }: PullRequest) => + <div id='status-checks'>{ + state === PullRequestStateEnum.Merged + ? 'Pull request successfully merged' + : + state === PullRequestStateEnum.Closed + ? 'This pull request is closed' + : + <div className='status-section'> + <div className='status-item'> + <StateIcon state={status.state} /> + <div>{getSummaryLabel(status.statuses)}</div> + </div> + </div> + }</div>; + +function getSummaryLabel(statuses: any[]) { + const statusTypes = groupBy(statuses, (status: any) => status.state); + let statusPhrases = []; + for (let statusType of Object.keys(statusTypes)) { + const numOfType = statusTypes[statusType].length; + let statusAdjective = ''; + + switch (statusType) { + case 'success': + statusAdjective = 'successful'; + break; + case 'failure': + statusAdjective = 'failed'; + break; + default: + statusAdjective = 'pending'; + } + + const status = numOfType > 1 + ? `${numOfType} ${statusAdjective} checks` + : `${numOfType} ${statusAdjective} check`; + + statusPhrases.push(status); + } + + console.log('statuses:', statuses); + console.log('status text:', statusPhrases.join(' and ')); + return statusPhrases.join(' and '); +} + +function StateIcon({ state }: { state: string }) { + return <div dangerouslySetInnerHTML={{ + __html: + state === 'success' + ? checkIcon + : + state === 'failure' + ? deleteIcon + : + pendingIcon + }}/>; +} \ No newline at end of file diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index e05329c282..929b2d71e0 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -689,7 +689,6 @@ export class PullRequestOverviewPanel { <title>Pull Request #${number} -
+ `; } From 1d5bee7f49d24c787180f13e4ee8d1888b3073b6 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Sat, 30 Mar 2019 11:34:37 +0800 Subject: [PATCH 11/50] Show and hide status checks. --- preview-src/index.css | 4 ++++ preview-src/views.tsx | 55 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/preview-src/index.css b/preview-src/index.css index f02f785cd6..a1c4474cbc 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -157,6 +157,10 @@ body .comment-container .review-comment-header { display: flex; } +.status-check-detail-text { + margin-left: 0.7em; +} + #confirm-merge { margin-left: auto; } diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 14fc7e455c..22a9e98124 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -26,7 +26,7 @@ export const Overview = (pr: PullRequest) =>
; -const Avatar = ({ for: author }: { for: PullRequest['author'] }) => +const Avatar = ({ for: author }: { for: Partial }) => ; @@ -170,7 +170,7 @@ const association = ({ authorAssociation }: ReviewEvent, import { groupBy } from 'lodash'; import { DiffHunk, DiffLine } from '../src/common/diffHunk'; -import { useContext } from 'react'; +import { useContext, useReducer } from 'react'; import { PullRequestStateEnum } from '../src/github/interface'; const positionKey = (comment: Comment) => @@ -279,8 +279,10 @@ const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Partial ; -const StatusChecks = ({ state, status }: PullRequest) => -
{ +const StatusChecks = ({ state, status }: PullRequest) => { + const [showDetails, toggleDetails] = useReducer(show => !show, false); + + return
{ state === PullRequestStateEnum.Merged ? 'Pull request successfully merged' : @@ -291,9 +293,52 @@ const StatusChecks = ({ state, status }: PullRequest) =>
{getSummaryLabel(status.statuses)}
+ { + showDetails ? 'Hide' : 'Show' + }
+ {showDetails ? + + : null}
}
; +}; + +const StatusCheckDetails = ({ statuses }: Partial) => +
{ + statuses.map(s => +
+ + + {s.context} — {s.description} + Details +
+ ) + }
; + +// const x = () => { + // const statusElement: HTMLDivElement = document.createElement('div'); + // statusElement.className = 'status-check'; + + // const state: HTMLSpanElement = document.createElement('span'); + // state.innerHTML = getStateIcon(s.state); + + // statusElement.appendChild(state); + + // const statusIcon = renderUserIcon(s.target_url, s.avatar_url); + // statusElement.appendChild(statusIcon); + + // const statusDescription = document.createElement('span'); + // statusDescription.textContent = `${s.context} - ${s.description}`; + // statusElement.appendChild(statusDescription); + + // const detailsLink = document.createElement('a'); + // detailsLink.textContent = 'Details'; + // detailsLink.href = s.target_url; + // statusElement.appendChild(detailsLink); + + // statusList.appendChild(statusElement); + // }) function getSummaryLabel(statuses: any[]) { const statusTypes = groupBy(statuses, (status: any) => status.state); @@ -326,7 +371,7 @@ function getSummaryLabel(statuses: any[]) { } function StateIcon({ state }: { state: string }) { - return
Date: Sat, 30 Mar 2019 12:33:31 +0800 Subject: [PATCH 12/50] WIP: Merge selector. --- preview-src/views.tsx | 103 +++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 22a9e98124..0244d530da 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -170,7 +170,7 @@ const association = ({ authorAssociation }: ReviewEvent, import { groupBy } from 'lodash'; import { DiffHunk, DiffLine } from '../src/common/diffHunk'; -import { useContext, useReducer } from 'react'; +import { useContext, useReducer, useRef, useState } from 'react'; import { PullRequestStateEnum } from '../src/github/interface'; const positionKey = (comment: Comment) => @@ -279,7 +279,8 @@ const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Partial
; -const StatusChecks = ({ state, status }: PullRequest) => { +const StatusChecks = (pr: PullRequest) => { + const { state, status, mergeable } = pr; const [showDetails, toggleDetails] = useReducer(show => !show, false); return
{ @@ -289,21 +290,73 @@ const StatusChecks = ({ state, status }: PullRequest) => { state === PullRequestStateEnum.Closed ? 'This pull request is closed' : -
-
- -
{getSummaryLabel(status.statuses)}
- { - showDetails ? 'Hide' : 'Show' - } -
- {showDetails ? - - : null} -
+ <> +
+
+ +
{getSummaryLabel(status.statuses)}
+ { + showDetails ? 'Hide' : 'Show' + } +
+ {showDetails ? + + : null} +
+ + { mergeable ? : null} + + }
; +}; + +const MergeStatus = ({ mergeable }: Pick) => +
+ +
{ + mergeable + ? 'This branch has no conflicts with the base branch' + : 'This branch has conflicts that must be resolved' + }
+
; + +const Merge = (pr: PullRequest) => { + const select = useRef(); + const [ selectedMethod, selectMethod ] = useState(null); + + return
{ + selectedMethod + ? + : + <> + + {nbsp}using method{nbsp} + + }
; }; +const MERGE_METHODS = { + merge: 'Create Merge Commit', + squash: 'Squash and Merge', + rebase: 'Rebase and Merge', +}; + +type MergeSelectProps = + Pick & + Pick; + +const MergeSelect = React.forwardRef(( + { defaultMergeMethod, mergeMethodsAvailability: avail }: MergeSelectProps, + ref) => + ); + const StatusCheckDetails = ({ statuses }: Partial) =>
{ statuses.map(s => @@ -370,15 +423,13 @@ function getSummaryLabel(statuses: any[]) { return statusPhrases.join(' and '); } -function StateIcon({ state }: { state: string }) { - return ; -} \ No newline at end of file +const StateIcon = ({ state }: { state: string }) => + ; \ No newline at end of file From 22765d8cb9dc8c73a9bf768f7eb5a9b395a3dd3d Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Sat, 30 Mar 2019 12:46:03 +0800 Subject: [PATCH 13/50] WIP: Merge option selector flow. --- preview-src/views.tsx | 44 +++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/preview-src/views.tsx b/preview-src/views.tsx index 0244d530da..4f469f1742 100644 --- a/preview-src/views.tsx +++ b/preview-src/views.tsx @@ -323,18 +323,42 @@ const Merge = (pr: PullRequest) => { const select = useRef(); const [ selectedMethod, selectMethod ] = useState(null); - return
{ - selectedMethod - ? - : - <> - - {nbsp}using method{nbsp} - - - }
; + if (selectedMethod) { + return selectMethod(null)} /> + } + + return
+ + {nbsp}using method{nbsp} + +
; }; +const ConfirmMerge = ({pr, method, cancel}: {pr: PullRequest, method: string, cancel: () => void}) => + <> + + -//
-// -// ${ 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; -// } -// } From 1539799b322a2d59601c2a9e4125776adc77a515 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 1 Apr 2019 23:36:10 +0800 Subject: [PATCH 20/50] Rename views.tsx to overview.tsx --- preview-src/app.tsx | 6 +++--- preview-src/{views.tsx => overview.tsx} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename preview-src/{views.tsx => overview.tsx} (100%) diff --git a/preview-src/app.tsx b/preview-src/app.tsx index 76bc11f5d1..67d80e97a6 100644 --- a/preview-src/app.tsx +++ b/preview-src/app.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { useContext, useState, useEffect } from 'react'; import { render } from 'react-dom'; -import { Overview } from './views'; -import PRContext from './context'; +import { Overview } from './overview'; +import PullRequestContext from './context'; import { PullRequest } from './cache'; export function main() { @@ -12,7 +12,7 @@ export function main() { } function Root({ children }) { - const ctx = useContext(PRContext); + const ctx = useContext(PullRequestContext); const [pr, setPR] = useState(ctx.pr); useEffect(() => { ctx.onchange = setPR; diff --git a/preview-src/views.tsx b/preview-src/overview.tsx similarity index 100% rename from preview-src/views.tsx rename to preview-src/overview.tsx From 8999d880473dbe463eb7fa50131656e0a47fb896 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Tue, 2 Apr 2019 00:26:50 +0800 Subject: [PATCH 21/50] Add MergedEvent to Timeline view. --- preview-src/timeline.tsx | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index 6a8c7b355f..ae48623827 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Comment } from '../src/common/comment'; import { TimelineEvent, isReviewEvent, isCommitEvent, isCommentEvent, isMergedEvent, isAssignEvent, ReviewEvent, CommitEvent, CommentEvent, MergedEvent, AssignEvent } from '../src/common/timelineEvent'; -import { commitIcon } from './icon'; +import { commitIcon, mergeIcon } from './icon'; import { Avatar, AuthorLink } from './user'; import { groupBy } from '../src/common/utils'; import { Spaced } from './space'; @@ -34,7 +34,7 @@ export const Timeline = ({ events }: { events: TimelineEvent[] }) => export default Timeline; -export const CommitEventView = (event: CommitEvent) => +const CommitEventView = (event: CommitEvent) =>
{commitIcon} @@ -93,5 +93,25 @@ const ReviewEventView = (event: ReviewEvent) => { }; const CommentEventView = (event: CommentEvent) => ; -const MergedEventView = (event: MergedEvent) =>

Merged: {event.id}

; -const AssignEventView = (event: AssignEvent) =>

Assign: {event.id}

; + +const MergedEventView = (event: MergedEvent) => +
+
+ {mergeIcon} +
+ +
+ +
+ 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; \ No newline at end of file From 3f79b6478939704dbeabccd3a8a4f7c85d3a0a6f Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Tue, 2 Apr 2019 00:29:30 +0800 Subject: [PATCH 22/50] Add Merged Events to Timeline view. --- preview-src/timeline.tsx | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index 6a8c7b355f..92179c39d8 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Comment } from '../src/common/comment'; import { TimelineEvent, isReviewEvent, isCommitEvent, isCommentEvent, isMergedEvent, isAssignEvent, ReviewEvent, CommitEvent, CommentEvent, MergedEvent, AssignEvent } from '../src/common/timelineEvent'; -import { commitIcon } from './icon'; +import { commitIcon, mergeIcon } from './icon'; import { Avatar, AuthorLink } from './user'; import { groupBy } from '../src/common/utils'; import { Spaced } from './space'; @@ -34,7 +34,7 @@ export const Timeline = ({ events }: { events: TimelineEvent[] }) => export default Timeline; -export const CommitEventView = (event: CommitEvent) => +const CommitEventView = (event: CommitEvent) =>
{commitIcon} @@ -93,5 +93,25 @@ const ReviewEventView = (event: ReviewEvent) => { }; const CommentEventView = (event: CommentEvent) => ; -const MergedEventView = (event: MergedEvent) =>

Merged: {event.id}

; -const AssignEventView = (event: AssignEvent) =>

Assign: {event.id}

; + +const MergedEventView = (event: MergedEvent) => +
+
+ {mergeIcon} +
+ +
+ +
+ 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; From 666ed3d944a6085002274d0cec1b8f40a44c7cd6 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 8 Apr 2019 14:10:51 +0530 Subject: [PATCH 23/50] Sidebar with reviewers and labels. --- preview-src/app.tsx | 2 +- preview-src/context.tsx | 7 ++++- preview-src/header.tsx | 4 +-- preview-src/icon.tsx | 4 +-- preview-src/index.css | 14 +++++++-- preview-src/index.ts | 2 +- preview-src/overview.tsx | 20 +++++++++---- preview-src/sidebar.tsx | 47 +++++++++++++++++++++++++++++++ preview-src/timeline.tsx | 8 +++--- src/github/pullRequestOverview.ts | 12 +------- 10 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 preview-src/sidebar.tsx diff --git a/preview-src/app.tsx b/preview-src/app.tsx index 67d80e97a6..1b90044d6e 100644 --- a/preview-src/app.tsx +++ b/preview-src/app.tsx @@ -8,7 +8,7 @@ import { PullRequest } from './cache'; export function main() { render( {pr => } - , document.getElementById('main')); + , document.getElementById('app')); } function Root({ children }) { diff --git a/preview-src/context.tsx b/preview-src/context.tsx index c9f945d552..ea45ad1b95 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -9,7 +9,6 @@ export class PRContext { public onchange: ((ctx: PullRequest) => void) | null = null, private _handler: MessageHandler = null) { if (!_handler) { - console.log('init message handler', this.handleMessage); this._handler = getMessageHandler(this.handleMessage); } } @@ -34,6 +33,12 @@ export class PRContext { public comment = (args: string) => this.postMessage({ command: 'pr.comment', args}) + public addReviewers = () => + this.postMessage({ command: 'pr.add-reviewers' }) + + public addLabels = () => + this.postMessage({ command: 'pr.add-labels' }) + setPR = (pr: PullRequest) => { this.pr = pr; setState(this.pr); diff --git a/preview-src/header.tsx b/preview-src/header.tsx index 111bebb28a..ffee5120cb 100644 --- a/preview-src/header.tsx +++ b/preview-src/header.tsx @@ -9,10 +9,10 @@ import PullRequestContext from './context'; import { checkIcon } from './icon'; import Timestamp from './timestamp'; -export const Header = ({ state, title, head, base, url, createdAt, author, isCurrentlyCheckedOut }: PullRequest) => +export const Header = ({ state, title, number, head, base, url, createdAt, author, }: PullRequest) => <>
-

{title}

+

{title} (#{number})

diff --git a/preview-src/icon.tsx b/preview-src/icon.tsx index 5a8bbebdb3..267f56579a 100644 --- a/preview-src/icon.tsx +++ b/preview-src/icon.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -export const Icon = ({ src }: { src: string }) => - ; +export const Icon = ({ className='', src, title }: { className?: string, title?: string, src: string }) => + ; export default Icon; diff --git a/preview-src/index.css b/preview-src/index.css index cc919a1101..2a789dc781 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; } @@ -938,7 +938,7 @@ code { } @media (max-width: 925px) { - body { + #app { display: block; } @@ -981,4 +981,14 @@ code { width: auto; margin-right: 4px; } +} + +.icon { + width: 1em; + height: 1em; + font-size: 16px; +} + +.push-right { + margin-left: auto; } \ No newline at end of file diff --git a/preview-src/index.ts b/preview-src/index.ts index 39cc6f2e40..d6dc693526 100644 --- a/preview-src/index.ts +++ b/preview-src/index.ts @@ -4,4 +4,4 @@ *--------------------------------------------------------------------------------------------*/ import './index.css'; import { main } from './app'; -main(); +addEventListener('load', main); diff --git a/preview-src/overview.tsx b/preview-src/overview.tsx index f38b9b9712..0bb62d4fba 100644 --- a/preview-src/overview.tsx +++ b/preview-src/overview.tsx @@ -5,17 +5,25 @@ import { Header } from './header'; import { AddComment, CommentBody } from './comment'; import Timeline from './timeline'; import StatusChecks from './merge'; +import Sidebar from './sidebar'; export const Overview = (pr: PullRequest) => <> -
-
+
+
+
+
+
+ +
+ + +
- - - ; const Description = (pr: PullRequest) => -
; +
+ +
; diff --git a/preview-src/sidebar.tsx b/preview-src/sidebar.tsx new file mode 100644 index 0000000000..48df4158da --- /dev/null +++ b/preview-src/sidebar.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { cloneElement, useContext } from 'react'; +import { PullRequest } from './cache'; +import { Avatar, AuthorLink } from './user'; +import { pendingIcon, commentIcon, checkIcon, diffIcon, plusIcon } from './icon'; +import PullRequestContext from './context'; + +export default function Sidebar({ reviewers, labels }: PullRequest) { + const { addReviewers, addLabels } = useContext(PullRequestContext); + return ; +} + +const REVIEW_STATE: { [state: string]: React.ReactElement } = { + REQUESTED: cloneElement(pendingIcon, { className: 'push-right', title: 'Awaiting requested review' }), + COMMENTED: cloneElement(commentIcon, { className: 'push-right', Root: 'div', title: 'Left review comments' }), + APPROVED: cloneElement(checkIcon, { className: 'push-right', title: 'Approved these changes' }), + CHANGES_REQUESTED: cloneElement(diffIcon, { className: 'push-right', title: 'Requested changes' }), +}; \ No newline at end of file diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index ae48623827..4789bb79cc 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -5,7 +5,7 @@ import { TimelineEvent, isReviewEvent, isCommitEvent, isCommentEvent, isMergedEv import { commitIcon, mergeIcon } from './icon'; import { Avatar, AuthorLink } from './user'; import { groupBy } from '../src/common/utils'; -import { Spaced } from './space'; +import { Spaced, nbsp } from './space'; import Timestamp from './timestamp'; import { CommentView } from './comment'; import Diff from './diff'; @@ -37,14 +37,14 @@ export default Timeline; const CommitEventView = (event: CommitEvent) =>
- {commitIcon} + {commitIcon}{nbsp}
{event.message}
- {event.sha.slice(0, 7)} + {event.sha.slice(0, 7)}
; const association = ( @@ -97,7 +97,7 @@ const CommentEventView = (event: CommentEvent) => ; const MergedEventView = (event: MergedEvent) =>
- {mergeIcon} + {mergeIcon}{nbsp}
diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 929b2d71e0..6e2dd2f914 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -689,17 +689,7 @@ export class PullRequestOverviewPanel { Pull Request #${number} -
- -
-
-
-
-
-
+
`; From 296c8d5b4757889adead347ae19166c003175526 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 8 Apr 2019 16:14:40 +0530 Subject: [PATCH 24/50] Action bar for editing and deleting comments. --- preview-src/comment.tsx | 69 +++++++++++++++++++++++++++++++++------- preview-src/context.tsx | 6 ++++ preview-src/index.css | 25 +++++++++++++-- preview-src/overview.tsx | 6 ++-- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 4c8877a923..fa258daf2c 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useContext } from 'react'; +import { useContext, useState } from 'react'; import Markdown from './markdown'; import { Spaced } from './space'; @@ -8,21 +8,69 @@ import Timestamp from './timestamp'; import { Comment } from '../src/common/comment'; import { PullRequest } from './cache'; import PullRequestContext from './context'; +import { editIcon, deleteIcon } from './icon'; -export const CommentView = ({ user, htmlUrl, createdAt, bodyHTML, body }: Partial) => -
+export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body }: Partial) { + const [ bodyMd, setBodyMd ] = useState(body); + const { deleteComment, editComment } = useContext(PullRequestContext); + const [inEditMode, setEditMode] = useState(false); + const [showActionBar, setShowActionBar] = useState(false); + + if (inEditMode) { + return setEditMode(false) + } + onSave={ + edited => { + editComment({ id: String(id), body: edited }); + setBodyMd(edited); + console.log('bodyHTML=', bodyHTML, 'bodyMd=', edited) + setEditMode(false); + } + } />; + } + + return
setShowActionBar(true)} + onMouseLeave={() => setShowActionBar(false)} + > + { ((canEdit || canDelete) && showActionBar) + ?
+ {canEdit ? : null} + {canDelete ? : null} +
+ : null + }
- - + + commented
- +
; +} + +function EditComment({ body, onCancel, onSave }: { body: string, onCancel: () => void, onSave: (body: string) => void}) { + return
{ + event.preventDefault(); + const { markdown }: any = event.target; + onSave(markdown.value); + } + }> + +
+ + + +
+
; +} + const CommentEventView = (event: CommentEvent) => ; const MergedEventView = (event: MergedEvent) => diff --git a/preview-src/user.tsx b/preview-src/user.tsx index d1b31f3442..38696de190 100644 --- a/preview-src/user.tsx +++ b/preview-src/user.tsx @@ -1,9 +1,10 @@ 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 }) => 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 d231eaf425..d626dcd221 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'; @@ -733,6 +733,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); @@ -750,6 +751,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(); @@ -1050,7 +1082,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(':'); @@ -1481,6 +1513,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 { @@ -1513,12 +1547,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 28c7c907f6..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, From fca5188e461c0772a8c93b0291e8ab3b2c968798 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Fri, 19 Apr 2019 16:21:38 -0400 Subject: [PATCH 27/50] Clicking on comments now brings us to the file. --- preview-src/comment.tsx | 10 ++++++++-- preview-src/context.tsx | 4 ++++ preview-src/diff.tsx | 11 ++++++++--- preview-src/timeline.tsx | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index d29216fee0..88b44927d5 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -52,8 +52,14 @@ export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, cre - commented - + { + createdAt + ? <> + commented + + + : pending + }
diff --git a/preview-src/context.tsx b/preview-src/context.tsx index c42dfa845b..1f106d8f8a 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -2,6 +2,7 @@ import { createContext } from 'react'; import { getMessageHandler, MessageHandler } from './message'; import { PullRequest, getState, setState } from './cache'; import { MergeMethod } from '../src/github/interface'; +import { Comment } from '../src/common/comment'; export class PRContext { constructor( @@ -54,6 +55,9 @@ export class PRContext { public submit = (body: string) => this.postMessage({ command: 'pr.submit', args: body }) + public openDiff = (comment: Comment) => + this.postMessage({ command: 'pr.open-diff', args: { comment } }) + setPR = (pr: PullRequest) => { this.pr = pr; setState(this.pr); diff --git a/preview-src/diff.tsx b/preview-src/diff.tsx index 9a67adee7a..3350bf0167 100644 --- a/preview-src/diff.tsx +++ b/preview-src/diff.tsx @@ -1,14 +1,19 @@ import * as React from 'react'; +import { useContext } from 'react'; +import { Comment } from '../src/common/comment'; import { DiffHunk, DiffLine } from '../src/common/diffHunk'; +import PullRequestContext from './context'; -export const Diff = ({ hunks, path, outdated=false }: { hunks: DiffHunk[], outdated: boolean, path: string }) => -
+function Diff({ comment, hunks, path, outdated=false }: { comment: Comment, hunks: DiffHunk[], outdated: boolean, path: string }) { + const { openDiff } = useContext(PullRequestContext); + return
{hunks.map(hunk => )}
; +} export default Diff; diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index a800674c83..6d5961d0a2 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -87,6 +87,7 @@ const ReviewEventView = (event: ReviewEvent) => { ([key, thread]) =>
From af1f7a7696159eee7f7d035dd46b2bb0d04a3fa2 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Fri, 19 Apr 2019 17:04:57 -0400 Subject: [PATCH 28/50] Hide status checks section if there aren't any. --- preview-src/merge.tsx | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/preview-src/merge.tsx b/preview-src/merge.tsx index 12182b2b78..3485bce335 100644 --- a/preview-src/merge.tsx +++ b/preview-src/merge.tsx @@ -20,18 +20,23 @@ export const StatusChecks = (pr: PullRequest) => { ? 'This pull request is closed' : <> -
-
- -
{getSummaryLabel(status.statuses)}
- { - showDetails ? 'Hide' : 'Show' - } -
- {showDetails ? - - : null} -
+ { status.statuses.length + ? <> +
+
+ +
{getSummaryLabel(status.statuses)}
+ { + showDetails ? 'Hide' : 'Show' + } +
+ {showDetails ? + + : null} +
+ + : null + } { mergeable ? : null} From c7cebbe0c809c0d5a7586c5c06a5b22d03a1ee0e Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Fri, 19 Apr 2019 17:07:13 -0400 Subject: [PATCH 29/50] Use a switch for StateIcon and remove pullRequestOverviewRenderer. --- preview-src/merge.tsx | 16 +- preview-src/pullRequestOverviewRenderer.ts | 1260 -------------------- 2 files changed, 7 insertions(+), 1269 deletions(-) delete mode 100644 preview-src/pullRequestOverviewRenderer.ts diff --git a/preview-src/merge.tsx b/preview-src/merge.tsx index 3485bce335..0608d0c07a 100644 --- a/preview-src/merge.tsx +++ b/preview-src/merge.tsx @@ -170,12 +170,10 @@ function getSummaryLabel(statuses: any[]) { return statusPhrases.join(' and '); } -const StateIcon = ({ state }: { state: string }) => - state === 'success' - ? checkIcon - : - state === 'failure' - ? deleteIcon - : - pendingIcon - ; +function StateIcon({ state }: { state: string }) { + switch (state) { + case 'success': return checkIcon; + case 'failure': return deleteIcon; + } + return pendingIcon; +} diff --git a/preview-src/pullRequestOverviewRenderer.ts b/preview-src/pullRequestOverviewRenderer.ts deleted file mode 100644 index 252968e0e9..0000000000 --- a/preview-src/pullRequestOverviewRenderer.ts +++ /dev/null @@ -1,1260 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { dateFromNow } from '../src/common/utils'; -import { TimelineEvent, CommitEvent, ReviewEvent, CommentEvent, isCommentEvent, isReviewEvent, isCommitEvent, isMergedEvent, MergedEvent } from '../src/common/timelineEvent'; -import { PullRequestStateEnum, ReviewState, MergeMethodsAvailability, ILabel, MergeMethod } from '../src/github/interface'; -import md from './mdRenderer'; -import { MessageHandler } from './message'; -import { getState, updateState, PullRequest } from './cache'; -import { Comment } from '../src/common/comment'; - -const commitIconSvg = require('../resources/icons/commit_icon.svg'); -const mergeIconSvg = require('../resources/icons/merge_icon.svg'); -const editIcon = require('../resources/icons/edit.svg'); -const checkIcon = require('../resources/icons/check.svg'); - -const plusIcon = require('../resources/icons/plus.svg'); -const deleteIcon = require('../resources/icons/delete.svg'); - -const pendingIcon = require('../resources/icons/dot.svg'); -const commentIcon = require('../resources/icons/comment.svg'); -const diffIcon = require('../resources/icons/diff.svg'); - -const emoji = require('node-emoji'); - -export const ElementIds = { - Checkout: 'checkout', - CheckoutDefaultBranch: 'checkout-default-branch', - Merge: 'merge', - Close: 'close', - Refresh: 'refresh', - Reply: 'reply', - Approve: 'approve', - RequestChanges: 'request-changes', - Status: 'status', - CommentTextArea: 'comment-textarea', - TimelineEvents: 'timeline-events' // If updating this value, change id in pullRequestOverview.ts as well. -}; - -export enum DiffChangeType { - Context, - Add, - Delete, - Control -} - -export function getDiffChangeType(text: string) { - let c = text[0]; - switch (c) { - case ' ': return DiffChangeType.Context; - case '+': return DiffChangeType.Add; - case '-': return DiffChangeType.Delete; - default: return DiffChangeType.Control; - } -} - -function groupBy(arr: T[], fn: (el: T) => string): { [key: string]: T[] } { - return arr.reduce((result, el) => { - const key = fn(el); - result[key] = [...(result[key] || []), el]; - return result; - }, Object.create(null)); -} - -function getSummaryLabel(statuses: any[]) { - const statusTypes = groupBy(statuses, (status: any) => status.state); - let statusPhrases = []; - for (let statusType of Object.keys(statusTypes)) { - const numOfType = statusTypes[statusType].length; - let statusAdjective = ''; - - switch (statusType) { - case 'success': - statusAdjective = 'successful'; - break; - case 'failure': - statusAdjective = 'failed'; - break; - default: - statusAdjective = 'pending'; - } - - const status = numOfType > 1 - ? `${numOfType} ${statusAdjective} checks` - : `${numOfType} ${statusAdjective} check`; - - statusPhrases.push(status); - } - - return statusPhrases.join(' and '); -} - -function getStateIcon(state: string) { - if (state === 'success') { - return checkIcon; - } else if (state === 'failure') { - return deleteIcon; - } else { - return pendingIcon; - } -} - -function setStatusCheckText(container: HTMLElement, state: PullRequestStateEnum) { - if (state === PullRequestStateEnum.Merged) { - container.innerHTML = 'Pull request successfully merged'; - } - - if (state === PullRequestStateEnum.Closed) { - container.innerHTML = 'This pull request is closed'; - } -} - -export function renderStatusChecks(pr: PullRequest, messageHandler: MessageHandler) { - const statusContainer = document.getElementById('status-checks') as HTMLDivElement; - statusContainer.innerHTML = ''; - - if (pr.state !== PullRequestStateEnum.Open) { - setStatusCheckText(statusContainer, pr.state); - return; - } - - const { status, mergeable } = pr; - - const statusCheckInformationContainer = document.createElement('div'); - statusCheckInformationContainer.classList.add('status-section'); - - const statusSummary = document.createElement('div'); - statusSummary.classList.add('status-item'); - const statusSummaryIcon = document.createElement('div'); - const statusSummaryText = document.createElement('div'); - statusSummaryIcon.innerHTML = getStateIcon(status.state); - statusSummary.appendChild(statusSummaryIcon); - statusSummaryText.textContent = getSummaryLabel(status.statuses); - statusSummary.appendChild(statusSummaryText); - statusCheckInformationContainer.appendChild(statusSummary); - - const statusesToggle = document.createElement('a'); - statusesToggle.setAttribute('aria-role', 'button'); - statusesToggle.textContent = status.state === 'success' ? 'Show' : 'Hide'; - statusesToggle.addEventListener('click', () => { - if (statusList.classList.contains('hidden')) { - statusList.classList.remove('hidden'); - statusesToggle.textContent = 'Hide'; - } else { - statusList.classList.add('hidden'); - statusesToggle.textContent = 'Show'; - } - }); - - statusSummary.appendChild(statusesToggle); - - if (!status.statuses.length) { - statusCheckInformationContainer.classList.add('hidden'); - } - - const statusList = document.createElement('div'); - if (status.state === 'success') { - statusList.classList.add('hidden'); - } - statusCheckInformationContainer.appendChild(statusList); - statusContainer.appendChild(statusCheckInformationContainer); - - status.statuses.forEach(s => { - const statusElement: HTMLDivElement = document.createElement('div'); - statusElement.className = 'status-check'; - - const state: HTMLSpanElement = document.createElement('span'); - state.innerHTML = getStateIcon(s.state); - - statusElement.appendChild(state); - - const statusIcon = renderUserIcon(s.target_url, s.avatar_url); - statusElement.appendChild(statusIcon); - - const statusDescription = document.createElement('span'); - statusDescription.textContent = `${s.context} - ${s.description}`; - statusElement.appendChild(statusDescription); - - const detailsLink = document.createElement('a'); - detailsLink.textContent = 'Details'; - detailsLink.href = s.target_url; - statusElement.appendChild(detailsLink); - - statusList.appendChild(statusElement); - }); - - const mergeableSummary = document.createElement('div'); - mergeableSummary.classList.add('status-item', 'status-section'); - const mergeableSummaryIcon = document.createElement('div'); - const mergeableSummaryText = document.createElement('div'); - mergeableSummaryIcon.innerHTML = mergeable ? checkIcon : deleteIcon; - mergeableSummary.appendChild(mergeableSummaryIcon); - mergeableSummaryText.textContent = mergeable ? 'This branch has no conflicts with the base branch' : 'This branch has conflicts that must be resolved'; - mergeableSummary.appendChild(mergeableSummaryText); - statusContainer.appendChild(mergeableSummary); - - renderMerge(pr, messageHandler, statusContainer); -} - -function renderMerge(pr: PullRequest, messageHandler: MessageHandler, container: HTMLElement) { - const mergeContainer = document.createElement('div'); - container.appendChild(mergeContainer); - - const mergeSelectorContainer = document.createElement('div'); - mergeSelectorContainer.classList.add('merge-select-container'); - const mergeButton = document.createElement('button'); - mergeButton.id = 'merge'; - mergeButton.textContent = 'Merge Pull Request'; - const mergeText = document.createElement('div'); - mergeText.textContent = 'using method'; - const mergeSelector = document.createElement('select'); - mergeSelector.innerHTML = getMergeOptions(pr.mergeMethodsAvailability); - mergeSelector.value = pr.defaultMergeMethod; - - mergeSelectorContainer.appendChild(mergeButton); - mergeSelectorContainer.appendChild(mergeText); - mergeSelectorContainer.appendChild(mergeSelector); - - mergeButton.addEventListener('click', () => { - if (mergeSelector.value !== 'rebase') { - mergeInputsContainer.classList.remove('hidden'); - } - mergeActionsContainer.classList.remove('hidden'); - mergeSelectorContainer.classList.add('hidden'); - - title.value = getDefaultTitleText(mergeSelector.value, pr); - description.value = getDefaultDescriptionText(mergeSelector.value, pr); - completeMergeButton.textContent = mergeSelector.selectedOptions[0].text; - }); - - const mergeInputsContainer = document.createElement('div'); - mergeInputsContainer.classList.add('hidden'); - const title = document.createElement('input'); - title.type = 'text'; - const description = document.createElement('textarea'); - description.placeholder = 'Add an optional extended description'; - - mergeInputsContainer.appendChild(title); - mergeInputsContainer.appendChild(description); - - const mergeActionsContainer = document.createElement('div'); - mergeActionsContainer.classList.add('hidden', 'form-actions'); - const completeMergeButton = document.createElement('button'); - completeMergeButton.id = 'confirm-merge'; - completeMergeButton.textContent = 'Confirm Merge'; - const cancelButton = document.createElement('button'); - cancelButton.textContent = 'Cancel'; - cancelButton.classList.add('secondary'); - - cancelButton.addEventListener('click', () => { - mergeInputsContainer.classList.add('hidden'); - mergeActionsContainer.classList.add('hidden'); - mergeSelectorContainer.classList.remove('hidden'); - }); - - completeMergeButton.addEventListener('click', () => { - completeMergeButton.disabled = true; - cancelButton.disabled = true; - messageHandler.postMessage({ - command: 'pr.merge', - args: { - title: title.value, - description: description.value, - method: mergeSelector.value - } - }).then(response => { - container.innerHTML = 'Pull request successfully merged'; - updatePullRequestState(response.state); - }).catch(_ => { - mergeInputsContainer.classList.add('hidden'); - mergeActionsContainer.classList.add('hidden'); - mergeSelectorContainer.classList.remove('hidden'); - - completeMergeButton.disabled = false; - cancelButton.disabled = false; - }); - }); - - mergeActionsContainer.appendChild(cancelButton); - mergeActionsContainer.appendChild(completeMergeButton); - - mergeContainer.appendChild(mergeSelectorContainer); - mergeContainer.appendChild(mergeInputsContainer); - mergeContainer.appendChild(mergeActionsContainer); -} - -function getMergeOptions(methodAvailability: MergeMethodsAvailability): string { - const methods: MergeMethod[] = ['merge', 'squash', 'rebase']; - - const options = methods.map(method => { - let optionText: string = ''; - switch (method) { - case 'merge': - optionText = 'Create Merge Commit'; - break; - case 'squash': - optionText = 'Squash and Merge'; - break; - case 'rebase': - optionText = 'Rebase and Merge'; - break; - } - - return ` - - `; - }); - - return options.join(''); -} - -function getDefaultTitleText(mergeMethod: string, pr: PullRequest) { - switch (mergeMethod) { - case 'merge': - return `Merge pull request #${pr.number} from ${pr.head}`; - case 'squash': - return pr.title; - default: - return ''; - } -} - -function getDefaultDescriptionText(mergeMethod: string, pr: PullRequest) { - return mergeMethod === 'merge' ? pr.title : ''; -} - -function renderUserIcon(iconLink: string, iconSrc: string): HTMLElement { - const iconContainer: HTMLDivElement = document.createElement('div'); - iconContainer.className = 'avatar-container'; - - const avatarLink: HTMLAnchorElement = document.createElement('a'); - avatarLink.className = 'avatar-link'; - avatarLink.href = iconLink; - - const avatar: HTMLImageElement = document.createElement('img'); - avatar.className = 'avatar'; - avatar.src = iconSrc; - - iconContainer.appendChild(avatarLink).appendChild(avatar); - - return iconContainer; -} - -export function updatePullRequestState(state: PullRequestStateEnum): void { - updateState({ state: state }); - - const merge = (document.getElementById(ElementIds.Merge)); - if (merge) { - const { mergeable } = getState(); - merge.disabled = !mergeable || state !== PullRequestStateEnum.Open; - } - - const close = (document.getElementById(ElementIds.Close)); - if (close) { - close.disabled = state !== PullRequestStateEnum.Open; - } - - const checkout = (document.getElementById(ElementIds.Checkout)); - if (checkout) { - checkout.disabled = checkout.disabled || state !== PullRequestStateEnum.Open; - } - - const approve = (document.getElementById(ElementIds.Approve)); - if (approve) { - approve.disabled = state !== PullRequestStateEnum.Open; - } - - const status = document.getElementById(ElementIds.Status); - status!.innerHTML = getStatus(state); - - if (state !== PullRequestStateEnum.Open) { - setStatusCheckText(document.getElementById('status-checks'), state); - } -} - -export interface ActionData { - body: string; - id: string; -} - -export class EditAction { - private _editingContainer: HTMLDivElement | undefined; - private _editingArea: HTMLTextAreaElement | undefined; - private _updateStateTimer: number = -1; - - constructor ( - private _data: ActionData | Comment, - private _renderedComment: HTMLElement, - private _messageHandler: MessageHandler, - private _updateHandler: (value: any) => void, - private _editCommand: string, - private _elementsToHide: HTMLElement[]) { - - } - - isEditing(): boolean { - return !!this._editingContainer; - } - - startEdit(text?: string): void { - this._editingContainer = document.createElement('div'); - this._editingContainer.className = 'editing-form'; - this._editingArea = document.createElement('textarea'); - this._editingArea.value = text || this._data.body; - - this._renderedComment.classList.add('hidden'); - this._elementsToHide.forEach(element => element.classList.add('hidden')); - - const cancelButton = document.createElement('button'); - cancelButton.textContent = 'Cancel'; - cancelButton.onclick = () => { this.finishEdit(); }; - - const updateButton = document.createElement('button'); - updateButton.textContent = 'Update'; - updateButton.onclick = () => { - this._messageHandler.postMessage({ - command: this._editCommand, - args: { - text: this._editingArea!.value, - comment: this._data - } - }).then(result => { - this.finishEdit(result.text); - }).catch(e => { - this.finishEdit(); - }); - - updateButton.textContent = 'Updating...'; - this._editingArea!.disabled = true; - updateButton.disabled = true; - }; - - const buttons = document.createElement('div'); - buttons.className = 'form-actions'; - buttons.appendChild(cancelButton); - buttons.appendChild(updateButton); - - this._editingContainer.appendChild(this._editingArea); - this._editingContainer.appendChild(buttons); - - this._renderedComment.parentElement!.appendChild(this._editingContainer); - this._editingArea.focus(); - - this._editingArea.addEventListener('input', (e) => { - const inputText = (e.target).value; - - if (this._updateStateTimer) { - clearTimeout(this._updateStateTimer); - } - - this._updateStateTimer = window.setTimeout(() => { - let pullRequest = getState(); - const pendingCommentDrafts = pullRequest.pendingCommentDrafts || Object.create(null); - pendingCommentDrafts[this._data.id] = inputText; - updateState({ pendingCommentDrafts: pendingCommentDrafts }); - }, 500); - }); - } - - private finishEdit(text?: string): void { - this._editingContainer!.remove(); - this._editingContainer = undefined; - this._editingArea = undefined; - - this._renderedComment.classList.remove('hidden'); - this._elementsToHide.forEach(element => element.classList.remove('hidden')); - - if (text !== undefined) { - this._data.body = text; - this._renderedComment.innerHTML = md.render(emoji.emojify(text)); - this._updateHandler(text); - } - - clearTimeout(this._updateStateTimer); - - let pullRequest = getState(); - const pendingCommentDrafts = pullRequest.pendingCommentDrafts; - if (pendingCommentDrafts) { - delete pendingCommentDrafts[this._data.id]; - updateState({ pendingCommentDrafts: pendingCommentDrafts }); - } - } -} - -export class ActionsBar { - private _actionsBar: HTMLDivElement | undefined; - private _editAction: EditAction | undefined; - - constructor(private _container: HTMLElement, - private _data: ActionData | Comment, - private _renderedComment: HTMLElement, - private _messageHandler: MessageHandler, - private _updateHandler: (value: any) => void, - private _editCommand?: string, - private _deleteCommand?: string, - private _review?: ReviewNode, - private _elementsToHide?: HTMLElement[]) { - - } - - render(): HTMLElement { - this._actionsBar = document.createElement('div'); - this._actionsBar.classList.add('comment-actions', 'hidden'); - - if (this._editCommand) { - const editButton = document.createElement('button'); - editButton.innerHTML = editIcon; - this._editAction = new EditAction(this._data, this._renderedComment, this._messageHandler, this._updateHandler, this._editCommand, (this._elementsToHide || []).concat(this._actionsBar)); - editButton.addEventListener('click', () => this._editAction.startEdit()); - this._actionsBar.appendChild(editButton); - } - - if (this._deleteCommand) { - const deleteButton = document.createElement('button'); - deleteButton.innerHTML = deleteIcon; - deleteButton.addEventListener('click', () => this.delete()); - this._actionsBar.appendChild(deleteButton); - } - - return this._actionsBar; - } - - registerActionBarListeners(): void { - this._container.addEventListener('mouseenter', () => { - if (this._editAction && !this._editAction.isEditing()) { - this._actionsBar!.classList.remove('hidden'); - } - }); - - this._container.addEventListener('focusin', () => { - if (this._editAction && !this._editAction.isEditing()) { - this._actionsBar!.classList.remove('hidden'); - } - }); - - this._container.addEventListener('mouseleave', () => { - if (!this._container.contains(document.activeElement)) { - this._actionsBar!.classList.add('hidden'); - } - }); - - this._container.addEventListener('focusout', (e) => { - if (!this._container.contains((e).target)) { - this._actionsBar!.classList.add('hidden'); - } - }); - } - - startEdit(text?: string): void { - if (this._editAction) { - this._editAction.startEdit(text); - } - } - - private delete(): void { - this._messageHandler.postMessage({ - command: this._deleteCommand, - args: this._data - }).then(_ => { - this._container.remove(); - if (this._review) { - this._review.deleteCommentFromReview(this._data as Comment); - } - - const pullRequest = getState(); - const index = pullRequest.events.findIndex(event => isCommentEvent(event) && event.id.toString() === this._data.id.toString()); - pullRequest.events.splice(index, 1); - updateState({ events: pullRequest.events }); - }); - } -} - -class CommentNode { - private _commentContainer: HTMLDivElement = document.createElement('div'); - private _commentBody: HTMLDivElement = document.createElement('div'); - private _actionsBar: ActionsBar | undefined; - - constructor(private _comment: Comment | CommentEvent, - private _messageHandler: MessageHandler, - private _review?: ReviewNode, - private _customEdit?: { - handler: (e: string) => void; - command: string; - }) { } - - render(): HTMLElement { - this._commentContainer.classList.add('comment-container', 'comment'); - - if (this._review) { - this._commentContainer.classList.add('review-comment'); - } - - const userIcon = renderUserIcon(this._comment.user.url, this._comment.user.avatarUrl); - const reviewCommentContainer: HTMLDivElement = document.createElement('div'); - reviewCommentContainer.className = 'review-comment-container'; - - this._commentContainer.appendChild(reviewCommentContainer); - - const commentHeader: HTMLDivElement = document.createElement('div'); - commentHeader.className = 'review-comment-header'; - const authorLink: HTMLAnchorElement = document.createElement('a'); - authorLink.className = 'author'; - authorLink.href = this._comment.user.url; - authorLink.textContent = this._comment.user.login; - - commentHeader.appendChild(userIcon); - commentHeader.appendChild(authorLink); - - if ((this._comment as Comment).isDraft) { - const pendingTag = document.createElement('a'); - pendingTag.className = 'pending'; - pendingTag.href = this._comment.htmlUrl; - pendingTag.textContent = 'Pending'; - - commentHeader.appendChild(pendingTag); - } else { - const timestamp: HTMLAnchorElement = document.createElement('a'); - timestamp.className = 'timestamp'; - timestamp.href = this._comment.htmlUrl; - timestamp.textContent = dateFromNow(this._comment.createdAt); - - const commentState = document.createElement('span'); - commentState.textContent = 'commented'; - - commentHeader.appendChild(commentState); - commentHeader.appendChild(timestamp); - } - - this._commentBody.className = 'comment-body'; - - this._commentBody.innerHTML = this._comment.bodyHTML ? this._comment.bodyHTML : md.render(emoji.emojify(this._comment.body)); - - if (this._comment.canEdit || this._comment.canDelete) { - this._actionsBar = new ActionsBar(this._commentContainer, - this._comment as Comment, - this._commentBody, - this._messageHandler, - this._customEdit && this._customEdit.handler, - this._comment.canEdit ? this._customEdit && this._customEdit.command || 'pr.edit-comment' : undefined, - this._comment.canDelete ? 'pr.delete-comment' : undefined, - this._review); - const actionBarElement = this._actionsBar.render(); - this._actionsBar.registerActionBarListeners(); - commentHeader.appendChild(actionBarElement); - } - - reviewCommentContainer.appendChild(commentHeader); - reviewCommentContainer.appendChild(this._commentBody); - - if (this._comment.body && this._comment.body.indexOf('```diff') > -1) { - const replyButton = document.createElement('button'); - replyButton.textContent = 'Apply Patch'; - replyButton.onclick = _ => { - this._messageHandler.postMessage({ - command: 'pr.apply-patch', - args: { - comment: this._comment - } - }); - }; - - this._commentBody.appendChild(replyButton); - } - - return this._commentContainer; - } - - startEdit(text?: string): void { - if (this._actionsBar) { - this._actionsBar.startEdit(text); - } - } -} - -export function renderComment( - comment: Comment | CommentEvent, - messageHandler: MessageHandler, - review?: ReviewNode, - customEdit?: { - handler: (e: string) => void - command: string - }) : HTMLElement { - const node = new CommentNode(comment, messageHandler, review, customEdit); - const { pendingCommentDrafts } = getState(); - const rendered = node.render(); - - if (pendingCommentDrafts) { - let text = pendingCommentDrafts[comment.id]; - if (pendingCommentDrafts[comment.id]) { - node.startEdit(text); - } - } - - return rendered; -} - -export function renderCommit(timelineEvent: CommitEvent): HTMLElement { - const shaShort = timelineEvent.sha.substring(0, 7); - - const commentContainer: HTMLDivElement = document.createElement('div'); - commentContainer.classList.add('comment-container', 'commit'); - const commitMessage: HTMLDivElement = document.createElement('div'); - commitMessage.className = 'commit-message'; - - commitMessage.insertAdjacentHTML('beforeend', commitIconSvg); - - const message: HTMLDivElement = document.createElement('div'); - message.className = 'message'; - if (timelineEvent.author && timelineEvent.author.url && timelineEvent.author.avatarUrl) { - const userIcon = renderUserIcon(timelineEvent.author.url, timelineEvent.author.avatarUrl); - commitMessage.appendChild(userIcon); - - const login: HTMLAnchorElement = document.createElement('a'); - login.className = 'author'; - login.href = timelineEvent.author.url; - login.textContent = timelineEvent.author.login!; - commitMessage.appendChild(login); - message.textContent = timelineEvent.message; - } else { - message.textContent = `${timelineEvent.author.login} ${timelineEvent.message}`; - } - - commitMessage.appendChild(message); - - const sha: HTMLAnchorElement = document.createElement('a'); - sha.className = 'sha'; - sha.href = timelineEvent.htmlUrl; - sha.textContent = shaShort; - - commentContainer.appendChild(commitMessage); - commentContainer.appendChild(sha); - - return commentContainer; -} - -export function renderMergedEvent(timelineEvent: MergedEvent): HTMLElement { - const shaShort = timelineEvent.sha.substring(0, 7); - - const mergedMessageContainer: HTMLDivElement = document.createElement('div'); - mergedMessageContainer.classList.add('comment-container', 'merged'); - const mergedMessage: HTMLDivElement = document.createElement('div'); - mergedMessage.className = 'merged-message'; - mergedMessage.insertAdjacentHTML('beforeend', mergeIconSvg); - - const userIcon = renderUserIcon(timelineEvent.user.url, timelineEvent.user.avatarUrl); - mergedMessage.appendChild(userIcon); - - const login: HTMLAnchorElement = document.createElement('a'); - login.className = 'author'; - login.href = timelineEvent.user.url; - login.textContent = timelineEvent.user.login!; - mergedMessage.appendChild(login); - - const message: HTMLSpanElement = document.createElement('span'); - message.className = 'message'; - message.textContent = 'merged commit'; - mergedMessage.appendChild(message); - - const sha: HTMLAnchorElement = document.createElement('a'); - sha.className = 'inline-sha'; - sha.href = timelineEvent.commitUrl; - sha.textContent = shaShort; - mergedMessage.appendChild(sha); - - const ref: HTMLSpanElement = document.createElement('span'); - ref.className = 'message'; - ref.textContent = `into ${timelineEvent.mergeRef}`; - mergedMessage.appendChild(ref); - - const timestamp: HTMLAnchorElement = document.createElement('a'); - timestamp.className = 'timestamp'; - timestamp.href = timelineEvent.url; - timestamp.textContent = dateFromNow(timelineEvent.createdAt); - mergedMessage.appendChild(timestamp); - - mergedMessageContainer.appendChild(mergedMessage); - return mergedMessageContainer; -} - -function getDiffChangeClass(type: DiffChangeType) { - switch (type) { - case DiffChangeType.Add: - return 'add'; - case DiffChangeType.Delete: - return 'delete'; - case DiffChangeType.Context: - return 'context'; - default: - return 'control'; - } -} - -export function renderReview(review: ReviewEvent, messageHandler: MessageHandler, supportsGraphQl: boolean): HTMLElement | undefined { - const reviewNode = new ReviewNode(review, messageHandler, supportsGraphQl); - return reviewNode.render(); -} - -class ReviewNode { - private _commentContainer: HTMLDivElement | undefined; - - constructor(private _review: ReviewEvent, private _messageHandler: MessageHandler, private _supportsGraphQl: boolean) { } - - isPending(): boolean { - return this._review.state.toLowerCase() === 'pending'; - } - - deleteCommentFromReview(comment: Comment): void { - const deletedCommentIndex = this._review.comments.findIndex(c => c.id.toString() === comment.id.toString()); - this._review.comments.splice(deletedCommentIndex, 1); - - if (!this._review.comments.length && !this._review.body) { - if (this._commentContainer) { - this._commentContainer.remove(); - this._commentContainer = undefined; - } - return; - } - - const commentsOnSameThread = this._review.comments.filter(c => c.path === comment.path && c.position === comment.position && c.originalPosition === comment.originalPosition); - if (!commentsOnSameThread.length) { - const path = comment.path + ':' + (comment.position !== null ? `pos:${comment.position}` : `ori:${comment.originalPosition}`); - const threadContainer = document.getElementById(path); - if (threadContainer) { - threadContainer.remove(); - } - } - - } - - render(): HTMLElement | undefined { - // Ignore pending or empty reviews - const isEmpty = !this._review.body && !(this._review.comments && this._review.comments.length); - - this._commentContainer = document.createElement('div'); - this._commentContainer.classList.add('comment-container', 'comment'); - const userIcon = renderUserIcon(this._review.user.url, this._review.user.avatarUrl); - - const commentHeader: HTMLDivElement = document.createElement('div'); - commentHeader.className = 'review-comment-header'; - - const userLogin: HTMLAnchorElement = document.createElement('a'); - userLogin.href = this._review.user.url; - userLogin.textContent = this._review.user.login; - - commentHeader.appendChild(userIcon); - commentHeader.appendChild(userLogin); - - if (this._review.authorAssociation && this._review.authorAssociation !== 'NONE') { - const authorAssociation: HTMLSpanElement = document.createElement('span'); - authorAssociation.textContent = `(${this._review.authorAssociation.toLocaleLowerCase()})`; - commentHeader.appendChild(authorAssociation); - } - - const reviewState = document.createElement('span'); - switch (this._review.state.toLowerCase()) { - case 'approved': - reviewState.textContent = ` approved these changes`; - break; - case 'commented': - reviewState.textContent = ` reviewed`; - break; - case 'changes_requested': - reviewState.textContent = ` requested changes`; - break; - default: - break; - } - - const timestamp: HTMLAnchorElement = document.createElement('a'); - timestamp.className = 'timestamp'; - timestamp.href = this._review.htmlUrl; - const isPending = this.isPending(); - timestamp.textContent = isPending ? 'Pending' : dateFromNow(this._review.submittedAt); - - if (isPending) { - timestamp.classList.add('pending'); - } - - commentHeader.appendChild(reviewState); - commentHeader.appendChild(timestamp); - - const reviewCommentContainer = document.createElement('div'); - reviewCommentContainer.className = 'review-comment-container'; - this._commentContainer.appendChild(reviewCommentContainer); - reviewCommentContainer.appendChild(commentHeader); - - if (isEmpty) { - return this._commentContainer; - } - - const reviewBody: HTMLDivElement = document.createElement('div'); - reviewBody.className = 'review-body'; - if (this._review.body) { - reviewBody.innerHTML = this._review.bodyHTML ? this._review.bodyHTML : md.render(emoji.emojify(this._review.body)); - reviewCommentContainer.appendChild(reviewBody); - } - - if (this._review.comments) { - const commentBody: HTMLDivElement = document.createElement('div'); - commentBody.classList.add('comment-body', 'review-comment-body'); - let groups = groupBy(this._review.comments, - comment => comment.path + ':' + (comment.position !== null ? `pos:${comment.position}` : `ori:${comment.originalPosition}`)); - - for (let path in groups) { - let comments = groups[path]; - const threadContainer: HTMLDivElement = document.createElement('div'); - threadContainer.id = path; - threadContainer.className = 'diff-container'; - - if (comments && comments.length) { - let diffLines: HTMLElement[] = []; - - for (let i = 0; i < comments[0].diffHunks.length; i++) { - diffLines = comments[0].diffHunks[i].diffLines.slice(-4).map(diffLine => { - const diffLineElement = document.createElement('div'); - diffLineElement.classList.add('diffLine', getDiffChangeClass(diffLine.type)); - - const oldLineNumber = document.createElement('span'); - oldLineNumber.textContent = diffLine.oldLineNumber > 0 ? diffLine.oldLineNumber.toString() : ' '; - oldLineNumber.classList.add('lineNumber'); - - const newLineNumber = document.createElement('span'); - newLineNumber.textContent = diffLine.newLineNumber > 0 ? diffLine.newLineNumber.toString() : ' '; - newLineNumber.classList.add('lineNumber'); - - const lineContent = document.createElement('span'); - lineContent.textContent = (diffLine as any)._raw; // the getter function has been stripped, directly access property - lineContent.classList.add('lineContent'); - - diffLineElement.appendChild(oldLineNumber); - diffLineElement.appendChild(newLineNumber); - diffLineElement.appendChild(lineContent); - - return diffLineElement; - }); - } - - let outdated = comments[0].position === null; - - const diffView: HTMLDivElement = document.createElement('div'); - diffView.className = 'diff'; - const diffHeader: HTMLDivElement = document.createElement('div'); - diffHeader.className = 'diffHeader'; - const diffPath: HTMLSpanElement = document.createElement('span'); - diffPath.className = outdated ? 'diffPath outdated' : 'diffPath'; - diffPath.textContent = comments[0].path; - diffHeader.appendChild(diffPath); - - if (outdated) { - const outdatedLabel: HTMLSpanElement = document.createElement('span'); - outdatedLabel.className = 'outdatedLabel'; - outdatedLabel.textContent = 'Outdated'; - diffHeader.appendChild(outdatedLabel); - } else { - diffPath.addEventListener('click', () => this.openDiff(comments[0])); - } - - diffView.appendChild(diffHeader); - diffLines.forEach(line => diffView.appendChild(line)); - - threadContainer.appendChild(diffView); - } - - comments.map(comment => threadContainer.appendChild(renderComment(comment, this._messageHandler, this))); - commentBody.appendChild(threadContainer); - } - - reviewCommentContainer.appendChild(commentBody); - - if (this.isPending() && this._supportsGraphQl) { - this.renderSubmitButtons(reviewCommentContainer); - } - } - - return this._commentContainer; - } - - private renderSubmitButtons(reviewCommentContainer: HTMLElement) { - const commentingContainer = document.createElement('div'); - commentingContainer.classList.add('comment-form'); - reviewCommentContainer.appendChild(commentingContainer); - - const commentingArea = document.createElement('textarea'); - commentingArea.placeholder = 'Leave a review summary comment'; - commentingContainer.appendChild(commentingArea); - - const formActions = document.createElement('div'); - formActions.classList.add('form-actions'); - commentingContainer.appendChild(formActions); - - this.renderSubmitButton('Request Changes', 'pr.request-changes', formActions, commentingArea); - this.renderSubmitButton('Approve', 'pr.approve', formActions, commentingArea); - this.renderSubmitButton('Submit', 'pr.submit', formActions, commentingArea); - } - - private renderSubmitButton(buttonText: string, buttonAction: string, container: HTMLElement, commentingArea: HTMLTextAreaElement) { - const submitButton = document.createElement('button'); - submitButton.id = buttonAction.slice(3); - submitButton.textContent = buttonText; - submitButton.addEventListener('click', () => { - submitButton.disabled = true; - this._messageHandler.postMessage({ - command: buttonAction, - args: commentingArea.value - }).then(message => { - appendReview(message, this._messageHandler); - }, err => { - // Handle error - submitButton.disabled = false; - }); - }); - - container.appendChild(submitButton); - } - - openDiff(comment: Comment) { - this._messageHandler.postMessage({ - command: 'pr.open-diff', - args: { - comment: comment - } - }); - } -} - -function renderSection(containerId: string, label: string, addCommand: string, renderItems: (newItems?: any[]) => HTMLElement[], messageHandler): void { - const container = document.getElementById(containerId); - container.innerHTML = ''; - - const sectionLabel = document.createElement('div'); - sectionLabel.className = 'section-header'; - - const sectionText = document.createElement('div'); - sectionText.textContent = label; - sectionLabel.appendChild(sectionText); - - const addButton = document.createElement('button'); - addButton.innerHTML = plusIcon; - addButton.title = `Add ${label}`; - addButton.addEventListener('click', () => { - messageHandler.postMessage({ - command: addCommand - }).then(message => { - const updatedItems = renderItems(message.added); - sectionContent.innerHTML = ''; - updatedItems.forEach(item => sectionContent.appendChild(item)); - }); - }); - sectionLabel.appendChild(addButton); - - const sectionContent = document.createElement('div'); - sectionContent.className = 'section-content'; - - container.appendChild(sectionLabel); - container.appendChild(sectionContent); - - const items = renderItems(); - items.forEach(item => sectionContent.appendChild(item)); -} - -function getReviewStateElement(state: string): HTMLElement { - const reviewState = document.createElement('div'); - - switch (state) { - case 'REQUESTED': - reviewState.innerHTML = pendingIcon; - reviewState.title = 'Awaiting requested review'; - break; - case 'COMMENTED': - reviewState.innerHTML = commentIcon; - reviewState.title = 'Left review comments'; - break; - case 'APPROVED': - reviewState.innerHTML = checkIcon; - reviewState.title = 'Approved these changes'; - break; - case 'CHANGES_REQUESTED': - reviewState.innerHTML = diffIcon; - reviewState.title = 'Requested changes'; - break; - } - return reviewState; -} - -export function renderDeleteButton(label: string, command: string, args: any, update: () => void, messageHandler: MessageHandler): HTMLElement { - const deleteButton = document.createElement('button'); - deleteButton.innerHTML = deleteIcon; - deleteButton.children[0].classList.add('hidden-focusable'); - deleteButton.title = `Remove ${label}`; - deleteButton.addEventListener('click', () => { - messageHandler.postMessage({ command, args }).then(_ => update()); - }); - - deleteButton.addEventListener('focus', () => { - deleteButton.children[0].classList.remove('hidden-focusable'); - }); - - deleteButton.addEventListener('blur', () => { - deleteButton.children[0].classList.add('hidden-focusable'); - }); - - return deleteButton; -} - -export function renderReviewers(pr: PullRequest, messageHandler: MessageHandler): void { - renderSection('reviewers', 'Reviewers', - 'pr.add-reviewers', - (newItems?: ReviewState[]): HTMLElement[] => { - if (newItems) { - pr.reviewers = pr.reviewers.concat(newItems); - updateState({ reviewers: pr.reviewers }); - } - - return pr.reviewers.map((reviewer, i) => { - const reviewerElement = document.createElement('div'); - reviewerElement.classList.add('section-item', 'reviewer'); - - const userIcon = renderUserIcon(reviewer.reviewer.url, reviewer.reviewer.avatarUrl); - reviewerElement.appendChild(userIcon); - - const userName = document.createElement('div'); - userName.className = 'login'; - reviewerElement.appendChild(userName); - userName.textContent = reviewer.reviewer.login; - - const reviewState = getReviewStateElement(reviewer.state); - reviewerElement.appendChild(reviewState); - - if (reviewer.state === 'REQUESTED') { - const deleteButton = renderDeleteButton('reviewer', 'pr.remove-reviewer', reviewer.reviewer.login, () => { - pr.reviewers.splice(i, 1); - updateState({ reviewers: pr.reviewers }); - reviewerElement.remove(); - }, messageHandler); - reviewerElement.appendChild(deleteButton); - - reviewerElement.addEventListener('mouseover', () => { - deleteButton.children[0].classList.remove('hidden-focusable'); - }); - - reviewerElement.addEventListener('mouseout', () => { - if (document.activeElement !== deleteButton) { - deleteButton.children[0].classList.add('hidden-focusable'); - } - }); - } - - return reviewerElement; - }); - }, messageHandler); -} - -export function renderLabels(pr: PullRequest, messageHandler: MessageHandler): void { - renderSection('labels', 'Labels', - 'pr.add-labels', - (newItems?: ILabel[]): HTMLElement[] => { - if (newItems) { - pr.labels = pr.labels.concat(newItems); - updateState({ labels: pr.labels }); - } - - return pr.labels.map((label, i) => { - const labelElement = document.createElement('div'); - labelElement.textContent = label.name; - labelElement.classList.add('label', 'section-item'); - - const deleteButton = renderDeleteButton('label', 'pr.remove-label', label.name, () => { - pr.labels.splice(i, 1); - updateState({ labels: pr.labels }); - labelElement.remove(); - }, messageHandler); - labelElement.appendChild(deleteButton); - labelElement.addEventListener('mouseover', () => { - deleteButton.children[0].classList.remove('hidden-focusable'); - }); - - labelElement.addEventListener('mouseout', () => { - if (document.activeElement !== deleteButton) { - deleteButton.children[0].classList.add('hidden-focusable'); - } - }); - - return labelElement; - }); - }, messageHandler); -} - -export function appendReview(data: { review: ReviewEvent, reviewers: ReviewState[] }, messageHandler: MessageHandler): void { - const state = getState(); - - let events = state.events; - if (state.supportsGraphQl) { - events = events.filter(e => !isReviewEvent(e) || e.state.toLowerCase() !== 'pending'); - events.forEach(event => { - if (isReviewEvent(event)) { - event.comments.forEach(c => c.isDraft = false); - } - }); - } - - events.push(data.review); - state.reviewers = data.reviewers; - updateState({ events: events, reviewers: state.reviewers }); - - renderTimelineEvents(state, messageHandler); - renderReviewers(state, messageHandler); - - clearTextArea(); -} - -export function clearTextArea() { - (document.getElementById(ElementIds.CommentTextArea)!).value = ''; - (document.getElementById(ElementIds.Reply)).disabled = true; - (document.getElementById(ElementIds.RequestChanges)).disabled = true; - - updateState({ pendingCommentText: undefined }); -} - -export function renderTimelineEvents(pr: PullRequest, messageHandler: MessageHandler): void { - const timelineElement = document.getElementById(ElementIds.TimelineEvents)!; - timelineElement.innerHTML = ''; - pr.events - .map(event => renderTimelineEvent(event, messageHandler, pr)) - .filter(event => event !== undefined) - .forEach(renderedEvent => timelineElement.appendChild(renderedEvent as HTMLElement)); -} - -export function renderTimelineEvent(timelineEvent: TimelineEvent, messageHandler: MessageHandler, state: PullRequest): HTMLElement | undefined { - if (isReviewEvent(timelineEvent)) { - return renderReview(timelineEvent, messageHandler, state.supportsGraphQl); - } - - if (isCommitEvent(timelineEvent)) { - return renderCommit(timelineEvent); - } - - if (isCommentEvent(timelineEvent)) { - return renderComment(timelineEvent, messageHandler); - } - - if (isMergedEvent(timelineEvent)) { - return renderMergedEvent(timelineEvent); - } - - return undefined; -} - -export function getStatus(state: PullRequestStateEnum) { - if (state === PullRequestStateEnum.Merged) { - return 'Merged'; - } else if (state === PullRequestStateEnum.Open) { - return 'Open'; - } else { - return 'Closed'; - } -} From c253a3d4bf406c87e959f819683c129e2331c5d8 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Fri, 19 Apr 2019 17:10:47 -0400 Subject: [PATCH 30/50] Update header to not reference PROverviewRenderer --- preview-src/header.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/preview-src/header.tsx b/preview-src/header.tsx index 60867307a9..ff64678601 100644 --- a/preview-src/header.tsx +++ b/preview-src/header.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import { useContext } from 'react'; import { PullRequest } from './cache'; -import { getStatus } from './pullRequestOverviewRenderer'; import { Avatar, AuthorLink } from './user'; import { Spaced } from './space'; import PullRequestContext from './context'; import { checkIcon } from './icon'; import Timestamp from './timestamp'; +import { PullRequestStateEnum } from '../src/github/interface'; export function Header({ state, title, number, head, base, url, createdAt, author, }: PullRequest) { const { refresh } = useContext(PullRequestContext); @@ -48,4 +48,14 @@ const CheckoutButtons = () => { } else { return ; } -}; \ No newline at end of file +}; + +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 From c1b64551eadb5b2ad5a4992c39ee896969b8c44e Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Fri, 19 Apr 2019 19:10:02 -0400 Subject: [PATCH 31/50] Rebase atop master, and fix action bar actions: - Fix action bar alignment. - Update timeline locally when we post a comment. - Append reviews to state. --- preview-src/comment.tsx | 27 +++++++------ preview-src/context.tsx | 84 ++++++++++++++++++++++++++++++++-------- preview-src/index.css | 1 + preview-src/timeline.tsx | 2 +- preview-src/user.tsx | 4 +- 5 files changed, 88 insertions(+), 30 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 88b44927d5..c44816b24a 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useContext, useState, useEffect } from 'react'; import Markdown from './markdown'; -import { Spaced } from './space'; +import { Spaced, nbsp } from './space'; import { Avatar, AuthorLink } from './user'; import Timestamp from './timestamp'; import { Comment } from '../src/common/comment'; @@ -10,7 +10,9 @@ import { PullRequest } from './cache'; import PullRequestContext from './context'; import { editIcon, deleteIcon } from './icon'; -export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body }: Partial) { +export type Props = Partial; + +export function CommentView({ id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, }: Props) { const [ bodyMd, setBodyMd ] = useState(body); const { deleteComment, editComment } = useContext(PullRequestContext); const [inEditMode, setEditMode] = useState(false); @@ -29,7 +31,7 @@ export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, cre } onSave={ edited => { - editComment({ id: String(id), body: edited }); + editComment({ id, pullRequestReviewId, body: edited }); setBodyMd(edited); setEditMode(false); } @@ -40,13 +42,6 @@ export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, cre onMouseEnter={() => setShowActionBar(true)} onMouseLeave={() => setShowActionBar(false)} > - { ((canEdit || canDelete) && showActionBar) - ?
- {canEdit ? : null} - {canDelete ? : null} -
- : null - }
@@ -55,12 +50,19 @@ export function CommentView({ id, canEdit, canDelete, user, author, htmlUrl, cre { createdAt ? <> - commented + commented{nbsp} : pending } + { ((canEdit || canDelete) && showActionBar) + ?
+ {canEdit ? : null} + {canDelete ? : null} +
+ : null + }
@@ -118,12 +120,13 @@ export function AddComment({ pendingCommentText }: PullRequest) { value='Comment' type='submit' className='reply-button' - disabled={!!pendingCommentText} /> + disabled={!pendingCommentText} />
; function onSubmit(evt) { evt.preventDefault(); + console.log('commenting:', (evt.target as any).body.value); comment((evt.target as any).body.value); } } \ No newline at end of file diff --git a/preview-src/context.tsx b/preview-src/context.tsx index 1f106d8f8a..f25b43034b 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -1,8 +1,9 @@ import { createContext } from 'react'; import { getMessageHandler, MessageHandler } from './message'; -import { PullRequest, getState, setState } from './cache'; +import { PullRequest, getState, setState, updateState } from './cache'; import { MergeMethod } from '../src/github/interface'; import { Comment } from '../src/common/comment'; +import { EventType, ReviewEvent, isReviewEvent } from '../src/common/timelineEvent'; export class PRContext { constructor( @@ -31,8 +32,15 @@ export class PRContext { public merge = (args: { title: string, description: string, method: MergeMethod }) => this.postMessage({ command: 'pr.merge', args }) - public comment = (args: string) => - this.postMessage({ command: 'pr.comment', args}) + public comment = async (args: string) => { + const result = await this.postMessage({ command: 'pr.comment', args}); + const newComment = result.value; + newComment.event = EventType.Commented; + this.updatePR({ + events: [...this.pr.events, newComment], + pendingCommentText: '', + }); + } public addReviewers = () => this.postMessage({ command: 'pr.add-reviewers' }) @@ -40,20 +48,60 @@ export class PRContext { public addLabels = () => this.postMessage({ command: 'pr.add-labels' }) - public deleteComment = (args: { id: string }) => - this.postMessage({ command: 'pr.delete-comment', args }) + public deleteComment = async (args: { id: number, pullRequestReviewId?: number }) => { + await this.postMessage({ command: 'pr.delete-comment', args }); + const { pr } = this; + const { id, pullRequestReviewId } = args; + if (!pullRequestReviewId) { + this.updatePR({ + events: pr.events.filter(e => e.id !== id) + }); + return; + } + const index = pr.events.findIndex(e => e.id === pullRequestReviewId); + if (index === -1) { + console.error('Could not find review:', pullRequestReviewId); + return; + } + const review: ReviewEvent = pr.events[index] as ReviewEvent; + if (!review.comments) { + console.error('No comments to delete for review:', pullRequestReviewId, review); + return; + } + this.pr.events.splice(index, 1, { + ...review, + comments: review.comments.filter(c => c.id !== id) + }); + this.updatePR(this.pr); + } - public editComment = (args: {id: string, body: string}) => + public editComment = (args: {id: number, pullRequestReviewId?: number, body: string}) => this.postMessage({ command: 'pr.edit-comment', args }) - public requestChanges = (body: string) => - this.postMessage({ command: 'pr.request-changes', args: body }) - - public approve = (body: string) => - this.postMessage({ command: 'pr.approve', args: body }) - - public submit = (body: string) => - this.postMessage({ command: 'pr.submit', args: body }) + public requestChanges = async (body: string) => + this.appendReview(await this.postMessage({ command: 'pr.request-changes', args: body })) + + public approve = async (body: string) => + this.appendReview(await this.postMessage({ command: 'pr.approve', args: body })) + + public submit = async (body: string) => + this.appendReview(await this.postMessage({ command: 'pr.submit', args: body })) + + private appendReview({ review, reviewers }: any) { + const state = this.pr; + let events = state.events; + if (state.supportsGraphQl) { + events = events.filter(e => !isReviewEvent(e) || e.state.toLowerCase() !== 'pending'); + events.forEach(event => { + if (isReviewEvent(event)) { + event.comments.forEach(c => c.isDraft = false); + } + }); + } + state.reviewers = reviewers; + state.events = [...state.events, review]; + this.updatePR(state); + } public openDiff = (comment: Comment) => this.postMessage({ command: 'pr.open-diff', args: { comment } }) @@ -65,8 +113,12 @@ export class PRContext { return this; } - updatePR = (pr: Partial) => - this.setPR({...this.pr, ...pr }) + updatePR = (pr: Partial) => { + updateState(pr); + this.pr = { ...this.pr, ...pr }; + if (this.onchange) { this.onchange(this.pr); } + return this; + } private postMessage(message: any) { console.log('sending', message); diff --git a/preview-src/index.css b/preview-src/index.css index 5c4e7c8d0b..05cee7124d 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -114,6 +114,7 @@ body .comment-container.review { } body .comment-container .review-comment-header { + position: relative; display: flex; width: 100%; box-sizing: border-box; diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index 6d5961d0a2..ea15e0fb14 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -91,7 +91,7 @@ const ReviewEventView = (event: ReviewEvent) => { hunks={thread[0].diffHunks} outdated={thread[0].position === null} path={thread[0].path} /> - {thread.map(c => )} + {thread.map(c => )}
) }
diff --git a/preview-src/user.tsx b/preview-src/user.tsx index 38696de190..695b3ad4f3 100644 --- a/preview-src/user.tsx +++ b/preview-src/user.tsx @@ -4,7 +4,9 @@ import { Icon } from './icon'; export const Avatar = ({ for: author }: { for: Partial }) => - {author.avatarUrl ? : } + {author.avatarUrl + ? + : } ; export const AuthorLink = ({ for: author, text=author.login }: { for: PullRequest['author'], text?: string }) => From 344835307cdc40a0fba38701dbf1d143c84bd15d Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 22 Apr 2019 14:39:48 -0400 Subject: [PATCH 32/50] Editing review comments now saves properly. --- preview-src/comment.tsx | 9 +++++---- preview-src/context.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index c44816b24a..4d6d63ca6f 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -12,7 +12,8 @@ import { editIcon, deleteIcon } from './icon'; export type Props = Partial; -export function CommentView({ id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, }: Props) { +export function CommentView(comment: Props) { + const { id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, } = comment; const [ bodyMd, setBodyMd ] = useState(body); const { deleteComment, editComment } = useContext(PullRequestContext); const [inEditMode, setEditMode] = useState(false); @@ -30,9 +31,9 @@ export function CommentView({ id, pullRequestReviewId, canEdit, canDelete, user, () => setEditMode(false) } onSave={ - edited => { - editComment({ id, pullRequestReviewId, body: edited }); - setBodyMd(edited); + text => { + editComment({ comment: comment as Comment, text }); + setBodyMd(text); setEditMode(false); } } />; diff --git a/preview-src/context.tsx b/preview-src/context.tsx index f25b43034b..c74a701901 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -75,8 +75,10 @@ export class PRContext { this.updatePR(this.pr); } - public editComment = (args: {id: number, pullRequestReviewId?: number, body: string}) => - this.postMessage({ command: 'pr.edit-comment', args }) + public editComment = async (args: {comment: Comment, text: string}) => { + console.log('editing', args); + console.log(await this.postMessage({ command: 'pr.edit-comment', args })); + } public requestChanges = async (body: string) => this.appendReview(await this.postMessage({ command: 'pr.request-changes', args: body })) From 9dcbb6f7b0faa611bc4a78d0bd2785d49f01f20c Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 22 Apr 2019 16:24:32 -0400 Subject: [PATCH 33/50] Comment editing and submit button hover. --- preview-src/context.tsx | 6 ++---- preview-src/index.css | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/preview-src/context.tsx b/preview-src/context.tsx index c74a701901..b19a842c3d 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -75,10 +75,8 @@ export class PRContext { this.updatePR(this.pr); } - public editComment = async (args: {comment: Comment, text: string}) => { - console.log('editing', args); - console.log(await this.postMessage({ command: 'pr.edit-comment', args })); - } + public editComment = (args: {comment: Comment, text: string}) => + this.postMessage({ command: 'pr.edit-comment', args }); public requestChanges = async (body: string) => this.appendReview(await this.postMessage({ command: 'pr.request-changes', args: body })) diff --git a/preview-src/index.css b/preview-src/index.css index 05cee7124d..58956aedaa 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -267,12 +267,12 @@ button, input[type=submit] { 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; } @@ -291,6 +291,7 @@ body button.checkedOut { body button.secondary, body button.secondary:hover { -webkit-filter: grayscale(100%); + filter: grayscale(100%); } body button svg { From 7e0343de3d91791b1a86fff90fc2bc99981ef160 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Mon, 22 Apr 2019 18:02:07 -0400 Subject: [PATCH 34/50] Removing labels and reviewers. --- preview-src/context.tsx | 14 ++++++++++++- preview-src/index.css | 5 +++++ preview-src/sidebar.tsx | 46 +++++++++++++++++++++++++++++------------ 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/preview-src/context.tsx b/preview-src/context.tsx index b19a842c3d..471b2d079b 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -1,7 +1,7 @@ import { createContext } from 'react'; import { getMessageHandler, MessageHandler } from './message'; import { PullRequest, getState, setState, updateState } from './cache'; -import { MergeMethod } from '../src/github/interface'; +import { MergeMethod, ReviewState } from '../src/github/interface'; import { Comment } from '../src/common/comment'; import { EventType, ReviewEvent, isReviewEvent } from '../src/common/timelineEvent'; @@ -87,6 +87,18 @@ export class PRContext { public submit = async (body: string) => this.appendReview(await this.postMessage({ command: 'pr.submit', args: body })) + public removeReviewer = async (review: ReviewState) => { + await this.postMessage({ command: 'pr.remove-reviewer', args: review }); + const reviewers = this.pr.reviewers.filter(r => r.reviewer.login !== review.reviewer.login); + this.updatePR({ reviewers }); + } + + public removeLabel = async (label: string) => { + await this.postMessage({ command: 'pr.remove-reviewer', args: label }); + const labels = this.pr.labels.filter(r => r.name !== label); + this.updatePR({ labels }); + } + private appendReview({ review, reviewers }: any) { const state = this.pr; let events = state.events; diff --git a/preview-src/index.css b/preview-src/index.css index 58956aedaa..c87d61ee64 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -1023,3 +1023,8 @@ code { margin-right: 4px; } + +.remove-item { + height: 12px; + cursor: pointer; +} diff --git a/preview-src/sidebar.tsx b/preview-src/sidebar.tsx index 48df4158da..dd4c960e03 100644 --- a/preview-src/sidebar.tsx +++ b/preview-src/sidebar.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { cloneElement, useContext } from 'react'; +import { cloneElement, useContext, useState } from 'react'; import { PullRequest } from './cache'; import { Avatar, AuthorLink } from './user'; -import { pendingIcon, commentIcon, checkIcon, diffIcon, plusIcon } from './icon'; +import { pendingIcon, commentIcon, checkIcon, diffIcon, plusIcon, deleteIcon } from './icon'; import PullRequestContext from './context'; +import { ReviewState, ILabel } from '../src/github/interface'; +import { nbsp } from './space'; export default function Sidebar({ reviewers, labels }: PullRequest) { const { addReviewers, addLabels } = useContext(PullRequestContext); @@ -14,12 +16,8 @@ export default function Sidebar({ reviewers, labels }: PullRequest) {
{ - reviewers.map(({ reviewer, state }) => -
- - - {REVIEW_STATE[state]} -
+ reviewers.map(state => + ) }
@@ -29,16 +27,38 @@ export default function Sidebar({ reviewers, labels }: PullRequest) {
{ - labels.map(({ name }) => -
- {name} -
- ) + labels.map(label =>
; } +function Reviewer(reviewState: ReviewState) { + const { reviewer, state } = reviewState; + const [ showDelete, setShowDelete ] = useState(false); + const { removeReviewer } = useContext(PullRequestContext); + return
setShowDelete(true)} + onMouseLeave={() => setShowDelete(false)}> + + + { showDelete ? <>{nbsp} removeReviewer(reviewState)}>{deleteIcon}️ : null} + {REVIEW_STATE[state]} +
; +} + +function Label(label: ILabel) { + const { name } = label; + const [ showDelete, setShowDelete ] = useState(false); + const { removeLabel } = useContext(PullRequestContext); + return
setShowDelete(true)} + onMouseLeave={() => setShowDelete(false)}> + {name} + { showDelete ? <>{nbsp} removeLabel(name)}>{deleteIcon}️{nbsp} : null} +
; +} + const REVIEW_STATE: { [state: string]: React.ReactElement } = { REQUESTED: cloneElement(pendingIcon, { className: 'push-right', title: 'Awaiting requested review' }), COMMENTED: cloneElement(commentIcon, { className: 'push-right', Root: 'div', title: 'Left review comments' }), From ca37507f6e3cd67cc8eafac948f826e2b960b513 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Tue, 30 Apr 2019 11:55:21 -0400 Subject: [PATCH 35/50] Fix lint errors and use edited body markdown. --- preview-src/comment.tsx | 2 +- preview-src/sidebar.tsx | 4 ++-- src/github/pullRequestManager.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 4d6d63ca6f..6b58e269ec 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -26,7 +26,7 @@ export function CommentView(comment: Props) { }, [body]); if (inEditMode) { - return setEditMode(false) } diff --git a/preview-src/sidebar.tsx b/preview-src/sidebar.tsx index dd4c960e03..fe27b062bc 100644 --- a/preview-src/sidebar.tsx +++ b/preview-src/sidebar.tsx @@ -38,8 +38,8 @@ function Reviewer(reviewState: ReviewState) { const [ showDelete, setShowDelete ] = useState(false); const { removeReviewer } = useContext(PullRequestContext); return
setShowDelete(true)} - onMouseLeave={() => setShowDelete(false)}> + onMouseEnter={state === 'REQUESTED' ? () => setShowDelete(true) : null} + onMouseLeave={state === 'REQUESTED' ? () => setShowDelete(false) : null}> { showDelete ? <>{nbsp} removeReviewer(reviewState)}>{deleteIcon}️ : null} diff --git a/src/github/pullRequestManager.ts b/src/github/pullRequestManager.ts index d626dcd221..9482f30d3b 100644 --- a/src/github/pullRequestManager.ts +++ b/src/github/pullRequestManager.ts @@ -751,20 +751,20 @@ export class PullRequestManager { } } - private async ensureTimelineEventAvatars(githubRepository: GitHubRepository, events: TimelineEvent[]): Promise{ + private async ensureTimelineEventAvatars(githubRepository: GitHubRepository, events: TimelineEvent[]): Promise { if (!events.length) { return; } let firstAvatarUrl: string | undefined = events.map(event => { - if(isCommitEvent(event)){ + if (isCommitEvent(event)) { return event.author.avatarUrl; - } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)){ + } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)) { return event.user.avatarUrl; } }) - .find(avatarUrl => !!avatarUrl) + .find(avatarUrl => !!avatarUrl); let repositoryReturnsAvatar = null; if (firstAvatarUrl) { @@ -773,9 +773,9 @@ export class PullRequestManager { if(repositoryReturnsAvatar === false) { events.forEach(event => { - if(isCommitEvent(event)){ + if (isCommitEvent(event)) { event.author.avatarUrl = undefined; - } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)){ + } else if(isReviewEvent(event) || isAssignEvent(event) || isCommentEvent(event) || isMergedEvent(event)) { event.user.avatarUrl = undefined; } }); From 2912cee49f5336fa3205eda7b0bcd62205c36067 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Tue, 30 Apr 2019 12:03:50 -0400 Subject: [PATCH 36/50] Revert staging server in githubServer.ts --- src/authentication/githubServer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/authentication/githubServer.ts b/src/authentication/githubServer.ts index 66fedaa3fe..f3cd223a9e 100644 --- a/src/authentication/githubServer.ts +++ b/src/authentication/githubServer.ts @@ -13,8 +13,7 @@ import { onDidChange as onKeychainDidChange, toCanonical, listHosts } from './ke const SCOPES: string = 'read:user user:email repo write:discussion'; const GHE_OPTIONAL_SCOPES: { [key: string]: boolean } = {'write:discussion': true}; -// const AUTH_RELAY_SERVER = 'https://vscode-auth.github.com'; -const AUTH_RELAY_SERVER = 'https://client-auth-staging-14a768b.herokuapp.com'; +const AUTH_RELAY_SERVER = 'https://vscode-auth.github.com'; const CALLBACK_PATH = '/did-authenticate'; const CALLBACK_URI = `${vscode.env.uriScheme}://${EXTENSION_ID}${CALLBACK_PATH}`; const MAX_TOKEN_RESPONSE_AGE = 5 * (1000 * 60 /* minutes in ms */); From 545e057ec702830100fc7532f2b1d206e9d58aa0 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan Date: Tue, 30 Apr 2019 12:32:54 -0400 Subject: [PATCH 37/50] Edit pr titles --- preview-src/comment.tsx | 1 - preview-src/context.tsx | 3 +++ preview-src/header.tsx | 56 +++++++++++++++++++++++++++++++++++------ preview-src/index.css | 4 +++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 6b58e269ec..40202f5021 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -127,7 +127,6 @@ export function AddComment({ pendingCommentText }: PullRequest) { function onSubmit(evt) { evt.preventDefault(); - console.log('commenting:', (evt.target as any).body.value); comment((evt.target as any).body.value); } } \ No newline at end of file diff --git a/preview-src/context.tsx b/preview-src/context.tsx index 471b2d079b..67e94b58db 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -15,6 +15,9 @@ export class PRContext { } } + public setTitle = (title: string) => + this.postMessage({ command: 'pr.edit-title', args: { text: title } }) + public checkout = () => this.postMessage({ command: 'pr.checkout' }) diff --git a/preview-src/header.tsx b/preview-src/header.tsx index ff64678601..91a3138b6d 100644 --- a/preview-src/header.tsx +++ b/preview-src/header.tsx @@ -1,21 +1,21 @@ import * as React from 'react'; -import { useContext } from 'react'; +import { useContext, useState } from 'react'; import { PullRequest } from './cache'; import { Avatar, AuthorLink } from './user'; import { Spaced } from './space'; import PullRequestContext from './context'; -import { checkIcon } from './icon'; +import { checkIcon, editIcon } from './icon'; import Timestamp from './timestamp'; import { PullRequestStateEnum } from '../src/github/interface'; -export function Header({ state, title, number, head, base, url, createdAt, author, }: PullRequest) { +export function Header({ canEdit, state, head, base, title, number, url, createdAt, author, isCurrentlyCheckedOut, }: PullRequest) { const { refresh } = useContext(PullRequestContext); return <>
-

{title} (#{number})

+ <div className='button-group'> - <CheckoutButtons /> + <CheckoutButtons {...{isCurrentlyCheckedOut}} /> <button onClick={refresh}>Refresh</button> </div> </div> @@ -38,9 +38,49 @@ export function Header({ state, title, number, head, base, url, createdAt, autho </>; } -const CheckoutButtons = () => { - const { pr, exitReviewMode, checkout } = useContext(PullRequestContext); - if (pr.isCurrentlyCheckedOut) { +function Title({ title, number, url, canEdit }: Partial<PullRequest>) { + const [ inEditMode, setEditMode ] = useState(false); + const [ showActionBar, setShowActionBar ] = useState(false); + const { setTitle } = useContext(PullRequestContext); + if (inEditMode) { + return <form + className='editing-form' + onSubmit={ + async evt => { + evt.preventDefault(); + try { + await setTitle((evt.target as any).text.value); + } finally { + setEditMode(false); + } + } + } + > + <textarea name='text'></textarea> + <div className='form-actions'> + <button>Cancel</button> + <input type='submit' value='Update' /> + </div> + </form>; + } + return <h2 className='pull-request-title' + onMouseEnter={() => setShowActionBar(true)} + onMouseLeave={() => setShowActionBar(false)} + > + {title} (<a href={url}>#{number}</a>) + { + (canEdit && showActionBar) + ? <div className='action-bar comment-actions'> + {canEdit ? <button onClick={() => setEditMode(true)}>{editIcon}</button> : null} + </div> + : null + } + </h2>; +} + +const CheckoutButtons = ({ isCurrentlyCheckedOut }) => { + const { exitReviewMode, checkout } = useContext(PullRequestContext); + if (isCurrentlyCheckedOut) { return <> <button aria-live='polite' className='checkedOut' disabled>{checkIcon} Checked Out</button> <button aria-live='polite' onClick={exitReviewMode}>Exit Review Mode</button> diff --git a/preview-src/index.css b/preview-src/index.css index c87d61ee64..3579e2c4c8 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -1028,3 +1028,7 @@ code { height: 12px; cursor: pointer; } + +.pull-request-title { + position: relative; +} \ No newline at end of file From 3fd16c5f63bf0e665d34a7565b21de303e588c90 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Thu, 2 May 2019 11:19:54 -0400 Subject: [PATCH 38/50] Handle failures during comment submission. --- preview-src/comment.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 40202f5021..f22a5bd635 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -31,10 +31,13 @@ export function CommentView(comment: Props) { () => setEditMode(false) } onSave={ - text => { - editComment({ comment: comment as Comment, text }); - setBodyMd(text); - setEditMode(false); + async text => { + try { + await editComment({ comment: comment as Comment, text }); + setBodyMd(text); + } finally { + setEditMode(false); + } } } />; } From 99e39dd48e329a714cefb8978e16500aabf219a3 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 3 May 2019 10:57:05 -0400 Subject: [PATCH 39/50] Default the status check view to open if any checks have failed. --- preview-src/merge.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/preview-src/merge.tsx b/preview-src/merge.tsx index 0608d0c07a..c0bf6393b0 100644 --- a/preview-src/merge.tsx +++ b/preview-src/merge.tsx @@ -10,7 +10,8 @@ import { nbsp } from './space'; export const StatusChecks = (pr: PullRequest) => { const { state, status, mergeable } = pr; - const [showDetails, toggleDetails] = useReducer(show => !show, false); + const [showDetails, toggleDetails] = useReducer(show => !show, + status.statuses.some(s => s.state === 'failure')); return <div id='status-checks'>{ state === PullRequestStateEnum.Merged From 940a2eda9633abf1ee19836aa062753cdc4c2337 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 3 May 2019 13:52:42 -0400 Subject: [PATCH 40/50] Fix title editing styles and ensure validation passes before setting the title. --- preview-src/comment.tsx | 33 +++++++++++++---- preview-src/context.tsx | 10 +++++- preview-src/header.tsx | 79 ++++++++++++++++++++++------------------- preview-src/index.css | 20 +++++++++-- 4 files changed, 95 insertions(+), 47 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index f22a5bd635..b45478db60 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useContext, useState, useEffect } from 'react'; +import { useContext, useState, useEffect, useRef } from 'react'; import Markdown from './markdown'; import { Spaced, nbsp } from './space'; @@ -15,8 +15,8 @@ export type Props = Partial<Comment & PullRequest>; export function CommentView(comment: Props) { const { id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, } = comment; const [ bodyMd, setBodyMd ] = useState(body); - const { deleteComment, editComment } = useContext(PullRequestContext); - const [inEditMode, setEditMode] = useState(false); + const { deleteComment, editComment, pr } = useContext(PullRequestContext); + const [inEditMode, setEditMode] = useState(!!(pr.pendingCommentDrafts && pr.pendingCommentDrafts[id])); const [showActionBar, setShowActionBar] = useState(false); useEffect(() => { @@ -26,7 +26,8 @@ export function CommentView(comment: Props) { }, [body]); if (inEditMode) { - return <EditComment body={bodyMd} + return <EditComment id={id} + body={bodyMd} onCancel={ () => setEditMode(false) } @@ -73,7 +74,18 @@ export function CommentView(comment: Props) { </div>; } -function EditComment({ body, onCancel, onSave }: { body: string, onCancel: () => void, onSave: (body: string) => void}) { +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( + () => { + console.log(JSON.stringify(draftComment.current)) + draftComment.current.dirty && updateDraft(id, draftComment.current.body) + }, + 500); + return () => clearInterval(interval); + }); return <form onSubmit={ event => { event.preventDefault(); @@ -81,7 +93,16 @@ function EditComment({ body, onCancel, onSave }: { body: string, onCancel: () => onSave(markdown.value); } }> - <textarea name='markdown' defaultValue={body} /> + <textarea + name='markdown' + defaultValue={body} + onInput={ + e => { + draftComment.current.body = (e.target as any).value; + draftComment.current.dirty = true; + } + } + /> <div className='form-actions'> <button className='secondary' onClick={onCancel}>Cancel</button> <input type='submit' value='Save' /> diff --git a/preview-src/context.tsx b/preview-src/context.tsx index 67e94b58db..40114c24ef 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -79,7 +79,15 @@ export class PRContext { } public editComment = (args: {comment: Comment, text: string}) => - this.postMessage({ command: 'pr.edit-comment', args }); + this.postMessage({ command: 'pr.edit-comment', args }) + + public updateDraft = (id: number, body: string) => { + let pullRequest = getState(); + const pendingCommentDrafts = pullRequest.pendingCommentDrafts || Object.create(null); + if (body === pendingCommentDrafts[id]) { return; } + pendingCommentDrafts[id] = body; + this.updatePR({ pendingCommentDrafts: pendingCommentDrafts }); + } public requestChanges = async (body: string) => this.appendReview(await this.postMessage({ command: 'pr.request-changes', args: body })) diff --git a/preview-src/header.tsx b/preview-src/header.tsx index 91a3138b6d..3fe1ca6b16 100644 --- a/preview-src/header.tsx +++ b/preview-src/header.tsx @@ -10,15 +10,8 @@ import Timestamp from './timestamp'; import { PullRequestStateEnum } from '../src/github/interface'; export function Header({ canEdit, state, head, base, title, number, url, createdAt, author, isCurrentlyCheckedOut, }: PullRequest) { - const { refresh } = useContext(PullRequestContext); return <> - <div className='overview-title'> - <Title {...{title, number, url, canEdit}} /> - <div className='button-group'> - <CheckoutButtons {...{isCurrentlyCheckedOut}} /> - <button onClick={refresh}>Refresh</button> - </div> - </div> + <Title {...{title, number, url, canEdit, isCurrentlyCheckedOut}} /> <div className='subtitle'> <div id='status'>{getStatus(state)}</div> <Avatar for={author} /> @@ -38,44 +31,56 @@ export function Header({ canEdit, state, head, base, title, number, url, created </>; } -function Title({ title, number, url, canEdit }: Partial<PullRequest>) { +function Title({ title, number, url, canEdit, isCurrentlyCheckedOut }: Partial<PullRequest>) { const [ inEditMode, setEditMode ] = useState(false); const [ showActionBar, setShowActionBar ] = useState(false); - const { setTitle } = useContext(PullRequestContext); - if (inEditMode) { - return <form - className='editing-form' - onSubmit={ - async evt => { - evt.preventDefault(); - try { - await setTitle((evt.target as any).text.value); - } finally { - setEditMode(false); + const [ currentTitle, setCurrentTitle ] = useState(title); + const { setTitle, refresh } = useContext(PullRequestContext); + const editableTitle = + inEditMode + ? + <form + className='editing-form title-editing-form' + onSubmit={ + async evt => { + evt.preventDefault(); + try { + const txt = (evt.target as any).text.value; + await setTitle(txt); + setCurrentTitle(txt); + } finally { + setEditMode(false); + } } } - } - > - <textarea name='text'></textarea> - <div className='form-actions'> - <button>Cancel</button> - <input type='submit' value='Update' /> - </div> - </form>; - } - return <h2 className='pull-request-title' - onMouseEnter={() => setShowActionBar(true)} - onMouseLeave={() => setShowActionBar(false)} - > - {title} (<a href={url}>#{number}</a>) + > + <textarea name='text' defaultValue={currentTitle}></textarea> + <div className='form-actions'> + <button className='secondary'>Cancel</button> + <input type='submit' value='Update' /> + </div> + </form> + : + <h2> + {currentTitle} (<a href={url}>#{number}</a>) + </h2>; + + return <div className='overview-title' + onMouseEnter={() => setShowActionBar(true)} + onMouseLeave={() => setShowActionBar(false)}> + {editableTitle} { - (canEdit && showActionBar) - ? <div className='action-bar comment-actions'> + (canEdit && showActionBar && !inEditMode) + ? <div className='flex-action-bar comment-actions'> {canEdit ? <button onClick={() => setEditMode(true)}>{editIcon}</button> : null} </div> : null } - </h2>; + <div className='button-group'> + <CheckoutButtons {...{isCurrentlyCheckedOut}} /> + <button onClick={refresh}>Refresh</button> + </div> + </div>; } const CheckoutButtons = ({ isCurrentlyCheckedOut }) => { diff --git a/preview-src/index.css b/preview-src/index.css index 3579e2c4c8..8189d293bc 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -312,6 +312,7 @@ body button.checkedOut svg { .overview-title { display: flex; + position: relative; } .overview-title h2 { @@ -1018,7 +1019,15 @@ code { right: 9px; } -.action-bar > button { +.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; } @@ -1029,6 +1038,11 @@ code { cursor: pointer; } -.pull-request-title { - position: relative; +.title-editing-form { + min-width: 30%; +} + +.title-editing-form > .form-actions > button { + margin-left: 0; + margin-right: 9px; } \ No newline at end of file From 863eb802d87ec1d6f478af75372c9e2daf5a3932 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 3 May 2019 14:02:43 -0400 Subject: [PATCH 41/50] Finally fix comment drafts with refs. --- preview-src/comment.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index b45478db60..1047330754 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -16,7 +16,8 @@ export function CommentView(comment: Props) { const { id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, } = comment; const [ bodyMd, setBodyMd ] = useState(body); const { deleteComment, editComment, pr } = useContext(PullRequestContext); - const [inEditMode, setEditMode] = useState(!!(pr.pendingCommentDrafts && pr.pendingCommentDrafts[id])); + const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id]; + const [inEditMode, setEditMode] = useState(!!currentDraft); const [showActionBar, setShowActionBar] = useState(false); useEffect(() => { @@ -27,7 +28,7 @@ export function CommentView(comment: Props) { if (inEditMode) { return <EditComment id={id} - body={bodyMd} + body={currentDraft || body} onCancel={ () => setEditMode(false) } @@ -80,8 +81,10 @@ function EditComment({ id, body, onCancel, onSave }: { id: number, body: string, useEffect(() => { const interval = setInterval( () => { - console.log(JSON.stringify(draftComment.current)) - draftComment.current.dirty && updateDraft(id, draftComment.current.body) + if (draftComment.current.dirty) { + updateDraft(id, draftComment.current.body); + draftComment.current.dirty = false; + } }, 500); return () => clearInterval(interval); From 80e216c5a02ed495fd2f7882d4eb5a34694c9b14 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Fri, 3 May 2019 14:06:20 -0400 Subject: [PATCH 42/50] Fix disabled state on submit inputs and fix disabling of request changes button. --- preview-src/comment.tsx | 2 +- preview-src/index.css | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 1047330754..efc19ad4de 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -140,7 +140,7 @@ export function AddComment({ pendingCommentText }: PullRequest) { <div className='form-actions'> <button id='close' className='secondary'>Close Pull Request</button> <button id='request-changes' - disabled={!!pendingCommentText} + disabled={!pendingCommentText} className='secondary'>Request Changes</button> <button id='approve' className='secondary'>Approve</button> diff --git a/preview-src/index.css b/preview-src/index.css index 8189d293bc..9e30b16268 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -647,7 +647,8 @@ textarea:focus { display: flex; } -body button:disabled { +body button:disabled, +input[type=submit]:disabled { opacity: 0.4; } From f8132839bc0ed275423a1451f05c7ae0070a68f4 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Mon, 6 May 2019 16:08:17 -0400 Subject: [PATCH 43/50] Invert default avatar icons when the theme is light. Co-Authored-By: stanleygoldman@github.com --- preview-src/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/preview-src/index.css b/preview-src/index.css index 9e30b16268..3cc5e74fd1 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -93,6 +93,10 @@ body span.avatar-icon svg { height: 24px; } +.vscode-light .avatar-icon { + filter: invert(100%); +} + body img.avatar { vertical-align: middle; } From 42e5c8bd06f230eadd8316e56a4264dfd7926fca Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Mon, 6 May 2019 16:26:51 -0400 Subject: [PATCH 44/50] Fix spacing form spacing and alignment. Co-Authored-By: stanleygoldman@github.com --- preview-src/comment.tsx | 2 +- preview-src/header.tsx | 5 +++-- preview-src/index.css | 14 ++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index efc19ad4de..b69535ea63 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -130,7 +130,7 @@ export const CommentBody = ({ bodyHTML, body }: Embodied) => export function AddComment({ pendingCommentText }: PullRequest) { const { updatePR, comment } = useContext(PullRequestContext); - return <form id='comment-form' className='comment-form' onSubmit={onSubmit}> + return <form id='comment-form' className='comment-form main-comment-form' onSubmit={onSubmit}> <textarea id='comment-textarea' name='body' onInput={({ target }) => diff --git a/preview-src/header.tsx b/preview-src/header.tsx index 3fe1ca6b16..1e5a6d533a 100644 --- a/preview-src/header.tsx +++ b/preview-src/header.tsx @@ -54,9 +54,10 @@ function Title({ title, number, url, canEdit, isCurrentlyCheckedOut }: Partial<P } } > - <textarea name='text' defaultValue={currentTitle}></textarea> + <textarea name='text' style={{ width: '100%' }} defaultValue={currentTitle}></textarea> <div className='form-actions'> - <button className='secondary'>Cancel</button> + <button className='secondary' + onClick={() => setEditMode(false)}>Cancel</button> <input type='submit' value='Update' /> </div> </form> diff --git a/preview-src/index.css b/preview-src/index.css index 3cc5e74fd1..0c1c27094a 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -636,19 +636,18 @@ textarea:focus { padding: 5px 0; } -.editing-form .form-actions button { - margin-left: 5px; -} - .reply-button { margin-left: auto; margin-right: 0 !important; } .form-actions { + display: flex; +} + +.main-comment-form > .form-actions { padding-top: 10px; margin-bottom: 10px; - display: flex; } body button:disabled, @@ -1044,10 +1043,9 @@ code { } .title-editing-form { - min-width: 30%; + flex-grow: 1; } -.title-editing-form > .form-actions > button { +.title-editing-form > .form-actions { margin-left: 0; - margin-right: 9px; } \ No newline at end of file From 399269671c2968ceb742308f0c1c91eddbe7096b Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Mon, 6 May 2019 16:39:58 -0400 Subject: [PATCH 45/50] Toggle the status checks open state when statuses update. Co-Authored-By: stanleygoldman@github.com --- preview-src/merge.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/preview-src/merge.tsx b/preview-src/merge.tsx index c0bf6393b0..6951e8f8df 100644 --- a/preview-src/merge.tsx +++ b/preview-src/merge.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { PullRequest } from './cache'; import PullRequestContext from './context'; import { groupBy } from 'lodash'; -import { useContext, useReducer, useRef, useState } from 'react'; +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'; @@ -10,8 +10,17 @@ 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')); + 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 <div id='status-checks'>{ state === PullRequestStateEnum.Merged From 5007102b4ccb314fec8de69d0dbf40ca9f498e45 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Mon, 6 May 2019 17:08:48 -0400 Subject: [PATCH 46/50] Retain comment frame when editing standalone comments. Co-Authored-By: stanleygoldman@github.com --- preview-src/comment.tsx | 86 +++++++++++++++++++++++++++++----------- preview-src/index.css | 1 + preview-src/timeline.tsx | 2 +- 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index b69535ea63..9badb85225 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -10,7 +10,9 @@ import { PullRequest } from './cache'; import PullRequestContext from './context'; import { editIcon, deleteIcon } from './icon'; -export type Props = Partial<Comment & PullRequest>; +export type Props = Partial<Comment & PullRequest> & { + headerInEditMode?: boolean +}; export function CommentView(comment: Props) { const { id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, } = comment; @@ -26,27 +28,70 @@ export function CommentView(comment: Props) { } }, [body]); - if (inEditMode) { - return <EditComment id={id} - body={currentDraft || body} - onCancel={ - () => setEditMode(false) + const header = + <Spaced> + <Avatar for={user || author} /> + <AuthorLink for={user || author} /> + { + createdAt + ? <> + commented{nbsp} + <Timestamp href={htmlUrl} date={createdAt} /> + </> + : <em>pending</em> } - onSave={ - async text => { - try { - await editComment({ comment: comment as Comment, text }); - setBodyMd(text); - } finally { - setEditMode(false); - } + </Spaced>; + + if (inEditMode) { + return React.cloneElement( + comment.headerInEditMode + ? <CommentBox for={comment} /> : <></>, {}, [ + <EditComment id={id} + body={currentDraft || body} + onCancel={ + () => setEditMode(false) } - } />; + onSave={ + async text => { + try { + await editComment({ comment: comment as Comment, text }); + setBodyMd(text); + } finally { + setEditMode(false); + } + } + } /> + ]); } - return <div className='comment-container comment review-comment' + return <CommentBox + for={comment} onMouseEnter={() => setShowActionBar(true)} onMouseLeave={() => setShowActionBar(false)} + >{ ((canEdit || canDelete) && showActionBar) + ? <div className='action-bar comment-actions'> + {canEdit ? <button onClick={() => setEditMode(true)}>{editIcon}</button> : null} + {canDelete ? <button onClick={() => deleteComment({ id, pullRequestReviewId })}>{deleteIcon}</button> : null} + </div> + : null + } + <CommentBody bodyHTML={bodyHTML} body={bodyMd} /> + </CommentBox>; +} + +type CommentBoxProps = { + for: Partial<Comment & PullRequest> + header?: React.ReactChild + onMouseEnter?: any + onMouseLeave?: any + children?: any +}; + +function CommentBox({ + for: { user, author, createdAt, htmlUrl }, + onMouseEnter, onMouseLeave, children }: CommentBoxProps) { + return <div className='comment-container comment review-comment' + {...{onMouseEnter, onMouseLeave}} > <div className='review-comment-container'> <div className='review-comment-header'> @@ -62,15 +107,8 @@ export function CommentView(comment: Props) { : <em>pending</em> } </Spaced> - { ((canEdit || canDelete) && showActionBar) - ? <div className='action-bar comment-actions'> - {canEdit ? <button onClick={() => setEditMode(true)}>{editIcon}</button> : null} - {canDelete ? <button onClick={() => deleteComment({ id, pullRequestReviewId })}>{deleteIcon}</button> : null} - </div> - : null - } </div> - <CommentBody bodyHTML={bodyHTML} body={bodyMd} /> + {children} </div> </div>; } diff --git a/preview-src/index.css b/preview-src/index.css index 0c1c27094a..3f29029485 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -115,6 +115,7 @@ body .comment-container.review { width: 100%; display: flex; flex-direction: column; + position: relative; } body .comment-container .review-comment-header { diff --git a/preview-src/timeline.tsx b/preview-src/timeline.tsx index ea15e0fb14..c4c9d0539e 100644 --- a/preview-src/timeline.tsx +++ b/preview-src/timeline.tsx @@ -120,7 +120,7 @@ function AddReviewSummaryComment() { </div>; } -const CommentEventView = (event: CommentEvent) => <CommentView {...event} />; +const CommentEventView = (event: CommentEvent) => <CommentView headerInEditMode {...event} />; const MergedEventView = (event: MergedEvent) => <div className='comment-container commit'> From 8b4f1b78db5acf2af0154e42b599bb3df7140a4b Mon Sep 17 00:00:00 2001 From: Stanley Goldman <Stanley.Goldman@gmail.com> Date: Mon, 6 May 2019 17:36:55 -0400 Subject: [PATCH 47/50] Checking if the repository returns avatars --- src/github/pullRequestManager.ts | 42 ++++++++++++++++++++++++-------- src/github/utils.ts | 8 +++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/github/pullRequestManager.ts b/src/github/pullRequestManager.ts index 9482f30d3b..268da75782 100644 --- a/src/github/pullRequestManager.ts +++ b/src/github/pullRequestManager.ts @@ -621,14 +621,20 @@ export class PullRequestManager { } async getReviewRequests(pullRequest: PullRequestModel): Promise<IAccount[]> { - 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<Comment[]> { @@ -667,7 +673,8 @@ export class PullRequestManager { */ private async getPullRequestReviewComments(pullRequest: PullRequestModel): Promise<Comment[]> { 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, @@ -675,7 +682,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; } @@ -816,7 +829,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({ @@ -827,7 +841,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); } @@ -967,7 +983,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({ @@ -980,7 +997,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); } @@ -1119,7 +1138,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, @@ -1128,7 +1148,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)); } 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, From 052642dd553fbbd52a7fb510d51878cbdb10276d Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Mon, 6 May 2019 17:37:07 -0400 Subject: [PATCH 48/50] Update PR description correctly. Co-Authored-By: stanleygoldman@github.com --- preview-src/comment.tsx | 29 +++++++++++------------------ preview-src/context.tsx | 3 +++ preview-src/overview.tsx | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 9badb85225..33e638a1ad 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -12,12 +12,13 @@ import { editIcon, deleteIcon } from './icon'; export type Props = Partial<Comment & PullRequest> & { headerInEditMode?: boolean + isPRDescription?: boolean }; export function CommentView(comment: Props) { - const { id, pullRequestReviewId, canEdit, canDelete, user, author, htmlUrl, createdAt, bodyHTML, body, } = comment; + const { id, pullRequestReviewId, canEdit, canDelete, bodyHTML, body, isPRDescription } = comment; const [ bodyMd, setBodyMd ] = useState(body); - const { deleteComment, editComment, pr } = useContext(PullRequestContext); + const { deleteComment, editComment, setDescription, pr } = useContext(PullRequestContext); const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id]; const [inEditMode, setEditMode] = useState(!!currentDraft); const [showActionBar, setShowActionBar] = useState(false); @@ -28,20 +29,6 @@ export function CommentView(comment: Props) { } }, [body]); - const header = - <Spaced> - <Avatar for={user || author} /> - <AuthorLink for={user || author} /> - { - createdAt - ? <> - commented{nbsp} - <Timestamp href={htmlUrl} date={createdAt} /> - </> - : <em>pending</em> - } - </Spaced>; - if (inEditMode) { return React.cloneElement( comment.headerInEditMode @@ -54,7 +41,11 @@ export function CommentView(comment: Props) { onSave={ async text => { try { - await editComment({ comment: comment as Comment, text }); + if (isPRDescription) { + await setDescription(text); + } else { + await editComment({ comment: comment as Comment, text }); + } setBodyMd(text); } finally { setEditMode(false); @@ -88,8 +79,10 @@ type CommentBoxProps = { }; function CommentBox({ - for: { user, author, createdAt, htmlUrl }, + for: comment, onMouseEnter, onMouseLeave, children }: CommentBoxProps) { + const { user, author, createdAt, htmlUrl } = comment; + console.log('comment=', comment) return <div className='comment-container comment review-comment' {...{onMouseEnter, onMouseLeave}} > diff --git a/preview-src/context.tsx b/preview-src/context.tsx index 40114c24ef..2c840d250b 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -18,6 +18,9 @@ export class PRContext { public setTitle = (title: string) => this.postMessage({ command: 'pr.edit-title', args: { text: title } }) + public setDescription = (description: string) => + this.postMessage({ command: 'pr.edit-description', args: { text: description } }) + public checkout = () => this.postMessage({ command: 'pr.checkout' }) diff --git a/preview-src/overview.tsx b/preview-src/overview.tsx index cd9a3070be..eff3ad4acd 100644 --- a/preview-src/overview.tsx +++ b/preview-src/overview.tsx @@ -17,7 +17,7 @@ export const Overview = (pr: PullRequest) => <Sidebar {...pr} /> <div id='main'> <div id='description'> - <CommentView {...pr} /> + <CommentView isPRDescription {...pr} /> </div> <Timeline events={pr.events} /> <StatusChecks {...pr} /> From 25728a143e3d0306fda5d74132006c286925f223 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Tue, 7 May 2019 13:52:28 -0400 Subject: [PATCH 49/50] Clear pendingCommentDrafts for comment on cancel. --- preview-src/comment.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/preview-src/comment.tsx b/preview-src/comment.tsx index 33e638a1ad..3d8f279ba8 100644 --- a/preview-src/comment.tsx +++ b/preview-src/comment.tsx @@ -36,7 +36,12 @@ export function CommentView(comment: Props) { <EditComment id={id} body={currentDraft || body} onCancel={ - () => setEditMode(false) + () => { + if (pr.pendingCommentDrafts) { + delete pr.pendingCommentDrafts[id]; + } + setEditMode(false); + } } onSave={ async text => { From 4a7c7d7ecb9034bac3496bb0f444f444ac810985 Mon Sep 17 00:00:00 2001 From: Ashi Krishnan <queerviolet@github.com> Date: Tue, 7 May 2019 13:57:02 -0400 Subject: [PATCH 50/50] Fix removing reviewers. --- preview-src/context.tsx | 6 +++--- preview-src/sidebar.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/preview-src/context.tsx b/preview-src/context.tsx index 2c840d250b..42ac726de5 100644 --- a/preview-src/context.tsx +++ b/preview-src/context.tsx @@ -101,9 +101,9 @@ export class PRContext { public submit = async (body: string) => this.appendReview(await this.postMessage({ command: 'pr.submit', args: body })) - public removeReviewer = async (review: ReviewState) => { - await this.postMessage({ command: 'pr.remove-reviewer', args: review }); - const reviewers = this.pr.reviewers.filter(r => r.reviewer.login !== review.reviewer.login); + public removeReviewer = async (login: string) => { + await this.postMessage({ command: 'pr.remove-reviewer', args: login }); + const reviewers = this.pr.reviewers.filter(r => r.reviewer.login !== login); this.updatePR({ reviewers }); } diff --git a/preview-src/sidebar.tsx b/preview-src/sidebar.tsx index fe27b062bc..6c9782d61f 100644 --- a/preview-src/sidebar.tsx +++ b/preview-src/sidebar.tsx @@ -42,7 +42,7 @@ function Reviewer(reviewState: ReviewState) { onMouseLeave={state === 'REQUESTED' ? () => setShowDelete(false) : null}> <Avatar for={reviewer} /> <AuthorLink for={reviewer} /> - { showDelete ? <>{nbsp}<a className='remove-item' onClick={() => removeReviewer(reviewState)}>{deleteIcon}️</a></> : null} + { showDelete ? <>{nbsp}<a className='remove-item' onClick={() => removeReviewer(reviewState.reviewer.login)}>{deleteIcon}️</a></> : null} {REVIEW_STATE[state]} </div>; }