Skip to content

Commit 1d7cff2

Browse files
author
Ashi Krishnan
authored
Merge pull request #1096 from microsoft/qv/react
Render the overview page with React
2 parents ce6bc9a + e69d060 commit 1d7cff2

28 files changed

+1319
-1655
lines changed

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@
527527
"@types/node": "*",
528528
"@types/node-fetch": "^2.1.4",
529529
"@types/query-string": "^6.1.1",
530+
"@types/react": "^16.8.4",
531+
"@types/react-dom": "^16.8.2",
530532
"@types/webpack": "^4.4.10",
531533
"@types/ws": "^5.1.2",
532534
"css-loader": "^0.28.11",
@@ -537,6 +539,8 @@
537539
"gulp-util": "^3.0.8",
538540
"minimist": "^1.2.0",
539541
"mocha": "^5.2.0",
542+
"react": "^16.8.2",
543+
"react-dom": "^16.8.2",
540544
"style-loader": "^0.21.0",
541545
"svg-inline-loader": "^0.8.0",
542546
"ts-loader": "^4.0.1",

preview-src/app.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
import { useContext, useState, useEffect } from 'react';
3+
import { render } from 'react-dom';
4+
import { Overview } from './overview';
5+
import PullRequestContext from './context';
6+
import { PullRequest } from './cache';
7+
8+
export function main() {
9+
render(
10+
<Root>{pr => <Overview {...pr} />}</Root>
11+
, document.getElementById('app'));
12+
}
13+
14+
function Root({ children }) {
15+
const ctx = useContext(PullRequestContext);
16+
const [pr, setPR] = useState<PullRequest>(ctx.pr);
17+
useEffect(() => {
18+
ctx.onchange = setPR;
19+
setPR(ctx.pr);
20+
}, []);
21+
return pr ? children(pr) : 'Loading...';
22+
}

preview-src/cache.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,18 @@ export interface PullRequest {
3636
}
3737

3838
export function getState(): PullRequest {
39-
return vscode.getState() || {};
39+
return vscode.getState();
4040
}
4141

4242
export function setState(pullRequest: PullRequest): void {
4343
let oldPullRequest = getState();
4444

45-
if (oldPullRequest.number && oldPullRequest.number === pullRequest.number) {
46-
pullRequest = Object.assign(pullRequest, {
47-
pendingCommentText: oldPullRequest.pendingCommentText
48-
});
45+
if (oldPullRequest &&
46+
oldPullRequest.number && oldPullRequest.number === pullRequest.number) {
47+
pullRequest.pendingCommentText = oldPullRequest.pendingCommentText;
4948
}
5049

51-
vscode.setState(pullRequest);
50+
if (pullRequest) { vscode.setState(pullRequest); }
5251
}
5352

5453
export function updateState(data: Partial<PullRequest>): void {

preview-src/comment.tsx

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import * as React from 'react';
2+
import { useContext, useState, useEffect, useRef } from 'react';
3+
4+
import Markdown from './markdown';
5+
import { Spaced, nbsp } from './space';
6+
import { Avatar, AuthorLink } from './user';
7+
import Timestamp from './timestamp';
8+
import { Comment } from '../src/common/comment';
9+
import { PullRequest } from './cache';
10+
import PullRequestContext from './context';
11+
import { editIcon, deleteIcon } from './icon';
12+
13+
export type Props = Partial<Comment & PullRequest> & {
14+
headerInEditMode?: boolean
15+
isPRDescription?: boolean
16+
};
17+
18+
export function CommentView(comment: Props) {
19+
const { id, pullRequestReviewId, canEdit, canDelete, bodyHTML, body, isPRDescription } = comment;
20+
const [ bodyMd, setBodyMd ] = useState(body);
21+
const { deleteComment, editComment, setDescription, pr } = useContext(PullRequestContext);
22+
const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id];
23+
const [inEditMode, setEditMode] = useState(!!currentDraft);
24+
const [showActionBar, setShowActionBar] = useState(false);
25+
26+
useEffect(() => {
27+
if (body !== bodyMd) {
28+
setBodyMd(body);
29+
}
30+
}, [body]);
31+
32+
if (inEditMode) {
33+
return React.cloneElement(
34+
comment.headerInEditMode
35+
? <CommentBox for={comment} /> : <></>, {}, [
36+
<EditComment id={id}
37+
body={currentDraft || body}
38+
onCancel={
39+
() => {
40+
if (pr.pendingCommentDrafts) {
41+
delete pr.pendingCommentDrafts[id];
42+
}
43+
setEditMode(false);
44+
}
45+
}
46+
onSave={
47+
async text => {
48+
try {
49+
if (isPRDescription) {
50+
await setDescription(text);
51+
} else {
52+
await editComment({ comment: comment as Comment, text });
53+
}
54+
setBodyMd(text);
55+
} finally {
56+
setEditMode(false);
57+
}
58+
}
59+
} />
60+
]);
61+
}
62+
63+
return <CommentBox
64+
for={comment}
65+
onMouseEnter={() => setShowActionBar(true)}
66+
onMouseLeave={() => setShowActionBar(false)}
67+
>{ ((canEdit || canDelete) && showActionBar)
68+
? <div className='action-bar comment-actions'>
69+
{canEdit ? <button onClick={() => setEditMode(true)}>{editIcon}</button> : null}
70+
{canDelete ? <button onClick={() => deleteComment({ id, pullRequestReviewId })}>{deleteIcon}</button> : null}
71+
</div>
72+
: null
73+
}
74+
<CommentBody bodyHTML={bodyHTML} body={bodyMd} />
75+
</CommentBox>;
76+
}
77+
78+
type CommentBoxProps = {
79+
for: Partial<Comment & PullRequest>
80+
header?: React.ReactChild
81+
onMouseEnter?: any
82+
onMouseLeave?: any
83+
children?: any
84+
};
85+
86+
function CommentBox({
87+
for: comment,
88+
onMouseEnter, onMouseLeave, children }: CommentBoxProps) {
89+
const { user, author, createdAt, htmlUrl } = comment;
90+
console.log('comment=', comment)
91+
return <div className='comment-container comment review-comment'
92+
{...{onMouseEnter, onMouseLeave}}
93+
>
94+
<div className='review-comment-container'>
95+
<div className='review-comment-header'>
96+
<Spaced>
97+
<Avatar for={user || author} />
98+
<AuthorLink for={user || author} />
99+
{
100+
createdAt
101+
? <>
102+
commented{nbsp}
103+
<Timestamp href={htmlUrl} date={createdAt} />
104+
</>
105+
: <em>pending</em>
106+
}
107+
</Spaced>
108+
</div>
109+
{children}
110+
</div>
111+
</div>;
112+
}
113+
114+
function EditComment({ id, body, onCancel, onSave }: { id: number, body: string, onCancel: () => void, onSave: (body: string) => void}) {
115+
const draftComment = useRef<{body: string, dirty: boolean}>({ body, dirty: false });
116+
const { updateDraft } = useContext(PullRequestContext);
117+
useEffect(() => {
118+
const interval = setInterval(
119+
() => {
120+
if (draftComment.current.dirty) {
121+
updateDraft(id, draftComment.current.body);
122+
draftComment.current.dirty = false;
123+
}
124+
},
125+
500);
126+
return () => clearInterval(interval);
127+
});
128+
return <form onSubmit={
129+
event => {
130+
event.preventDefault();
131+
const { markdown }: any = event.target;
132+
onSave(markdown.value);
133+
}
134+
}>
135+
<textarea
136+
name='markdown'
137+
defaultValue={body}
138+
onInput={
139+
e => {
140+
draftComment.current.body = (e.target as any).value;
141+
draftComment.current.dirty = true;
142+
}
143+
}
144+
/>
145+
<div className='form-actions'>
146+
<button className='secondary' onClick={onCancel}>Cancel</button>
147+
<input type='submit' value='Save' />
148+
</div>
149+
</form>;
150+
}
151+
152+
export interface Embodied {
153+
bodyHTML?: string;
154+
body?: string;
155+
}
156+
157+
export const CommentBody = ({ bodyHTML, body }: Embodied) =>
158+
body
159+
? <Markdown className='comment-body' src={body} />
160+
:
161+
bodyHTML
162+
? <div className='comment-body'
163+
dangerouslySetInnerHTML={ {__html: bodyHTML }} />
164+
:
165+
<div className='comment-body'><em>No description provided.</em></div>;
166+
167+
export function AddComment({ pendingCommentText }: PullRequest) {
168+
const { updatePR, comment } = useContext(PullRequestContext);
169+
return <form id='comment-form' className='comment-form main-comment-form' onSubmit={onSubmit}>
170+
<textarea id='comment-textarea'
171+
name='body'
172+
onInput={({ target }) =>
173+
updatePR({ pendingCommentText: (target as any).value })}
174+
value={pendingCommentText}
175+
placeholder='Leave a comment' />
176+
<div className='form-actions'>
177+
<button id='close' className='secondary'>Close Pull Request</button>
178+
<button id='request-changes'
179+
disabled={!pendingCommentText}
180+
className='secondary'>Request Changes</button>
181+
<button id='approve'
182+
className='secondary'>Approve</button>
183+
<input id='reply'
184+
value='Comment'
185+
type='submit'
186+
className='reply-button'
187+
disabled={!pendingCommentText} />
188+
</div>
189+
</form>;
190+
191+
function onSubmit(evt) {
192+
evt.preventDefault();
193+
comment((evt.target as any).body.value);
194+
}
195+
}

0 commit comments

Comments
 (0)