提交 e730ddb0 编写于 作者: T Tomas Vik

Merge branch '339-respond-in-diff-thread' into 'main'

refactor: prepare the code for thread replies

See merge request gitlab-org/gitlab-vscode-extension!208
...@@ -64,7 +64,6 @@ export class MrItemModel extends ItemModel { ...@@ -64,7 +64,6 @@ export class MrItemModel extends ItemModel {
const discussions = await gitlabService.getDiscussions({ const discussions = await gitlabService.getDiscussions({
issuable: this.mr, issuable: this.mr,
includePosition: true,
}); });
const discussionsOnDiff = discussions.filter(isTextDiffDiscussion); const discussionsOnDiff = discussions.filter(isTextDiffDiscussion);
const threads = discussionsOnDiff.map(discussion => { const threads = discussionsOnDiff.map(discussion => {
......
...@@ -36,6 +36,13 @@ interface GqlSnippetProject { ...@@ -36,6 +36,13 @@ interface GqlSnippetProject {
snippets: Node<GqlSnippet>; snippets: Node<GqlSnippet>;
} }
interface CreateNoteResult {
createNote: {
errors: unknown[];
note: GqlNote | null;
};
}
export interface GqlSnippet { export interface GqlSnippet {
id: string; id: string;
projectId: string; projectId: string;
...@@ -88,6 +95,7 @@ export type GqlTextPosition = GqlOldPosition | GqlNewPosition; ...@@ -88,6 +95,7 @@ export type GqlTextPosition = GqlOldPosition | GqlNewPosition;
interface GqlNotePermissions { interface GqlNotePermissions {
resolveNote: boolean; resolveNote: boolean;
adminNote: boolean; adminNote: boolean;
createNote: boolean;
} }
interface GqlGenericNote<T extends GqlBasePosition | null> { interface GqlGenericNote<T extends GqlBasePosition | null> {
...@@ -101,21 +109,25 @@ interface GqlGenericNote<T extends GqlBasePosition | null> { ...@@ -101,21 +109,25 @@ interface GqlGenericNote<T extends GqlBasePosition | null> {
position: T; position: T;
} }
interface GqlGenericDiscussion<T extends GqlBasePosition | null> { interface GqlGenericDiscussion<T extends GqlNote> {
replyId: string; replyId: string;
createdAt: string; createdAt: string;
resolved: boolean; resolved: boolean;
resolvable: boolean; resolvable: boolean;
notes: Node<GqlGenericNote<T>>; notes: Node<T>;
} }
export type GqlTextDiffNote = GqlGenericNote<GqlTextPosition>; export type GqlTextDiffNote = GqlGenericNote<GqlTextPosition>;
export type GqlTextDiffDiscussion = GqlGenericDiscussion<GqlTextPosition>; type GqlImageNote = GqlGenericNote<GqlImagePosition>;
export type GqlOverviewNote = GqlGenericNote<null>;
export type GqlNote = GqlTextDiffNote | GqlImageNote | GqlOverviewNote;
export type GqlDiscussion = export type GqlDiscussion =
| GqlGenericDiscussion<GqlTextPosition> | GqlGenericDiscussion<GqlTextDiffNote>
| GqlGenericDiscussion<GqlImagePosition> | GqlGenericDiscussion<GqlImageNote>
| GqlGenericDiscussion<null>; | GqlGenericDiscussion<GqlOverviewNote>;
export type GqlTextDiffDiscussion = GqlGenericDiscussion<GqlTextDiffNote>;
interface GqlDiscussionsProject { interface GqlDiscussionsProject {
mergeRequest?: { mergeRequest?: {
...@@ -137,7 +149,6 @@ type Note = GqlDiscussion | RestLabelEvent; ...@@ -137,7 +149,6 @@ type Note = GqlDiscussion | RestLabelEvent;
interface GetDiscussionsOptions { interface GetDiscussionsOptions {
issuable: RestIssuable; issuable: RestIssuable;
includePosition?: boolean;
endCursor?: string; endCursor?: string;
} }
...@@ -233,8 +244,31 @@ const positionFragment = gql` ...@@ -233,8 +244,31 @@ const positionFragment = gql`
} }
`; `;
const createDiscussionsFragment = (includePosition: boolean) => gql` const noteDetailsFragment = gql`
${includePosition ? positionFragment : ''} ${positionFragment}
fragment noteDetails on Note {
id
createdAt
system
author {
avatarUrl
name
username
webUrl
}
body
bodyHtml
userPermissions {
resolveNote
adminNote
createNote
}
...position
}
`;
const discussionsFragment = gql`
${noteDetailsFragment}
fragment discussions on DiscussionConnection { fragment discussions on DiscussionConnection {
pageInfo { pageInfo {
hasNextPage hasNextPage
...@@ -251,30 +285,15 @@ ${includePosition ? positionFragment : ''} ...@@ -251,30 +285,15 @@ ${includePosition ? positionFragment : ''}
endCursor endCursor
} }
nodes { nodes {
id ...noteDetails
createdAt
system
author {
avatarUrl
name
username
webUrl
}
body
bodyHtml
userPermissions {
resolveNote
adminNote
}
${includePosition ? `...position` : ''}
} }
} }
} }
} }
`; `;
const constructGetDiscussionsQuery = (isMr: boolean, includePosition = false) => gql` const constructGetDiscussionsQuery = (isMr: boolean) => gql`
${createDiscussionsFragment(includePosition)} ${discussionsFragment}
query Get${ query Get${
isMr ? 'Mr' : 'Issue' isMr ? 'Mr' : 'Issue'
}Discussions($projectPath: ID!, $iid: String!, $afterCursor: String) { }Discussions($projectPath: ID!, $iid: String!, $afterCursor: String) {
...@@ -298,9 +317,13 @@ const discussionSetResolved = gql` ...@@ -298,9 +317,13 @@ const discussionSetResolved = gql`
`; `;
const createNoteMutation = gql` const createNoteMutation = gql`
${noteDetailsFragment}
mutation CreateNote($issuableId: NoteableID!, $body: String!, $replyId: DiscussionID) { mutation CreateNote($issuableId: NoteableID!, $body: String!, $replyId: DiscussionID) {
createNote(input: { noteableId: $issuableId, body: $body, discussionId: $replyId }) { createNote(input: { noteableId: $issuableId, body: $body, discussionId: $replyId }) {
errors errors
note {
...noteDetails
}
} }
} }
`; `;
...@@ -472,13 +495,9 @@ export class GitLabNewService { ...@@ -472,13 +495,9 @@ export class GitLabNewService {
} as GqlDiscussion; } as GqlDiscussion;
} }
async getDiscussions({ async getDiscussions({ issuable, endCursor }: GetDiscussionsOptions): Promise<GqlDiscussion[]> {
issuable,
includePosition = false,
endCursor,
}: GetDiscussionsOptions): Promise<GqlDiscussion[]> {
const projectPath = getProjectPath(issuable); const projectPath = getProjectPath(issuable);
const query = constructGetDiscussionsQuery(isMr(issuable), includePosition); const query = constructGetDiscussionsQuery(isMr(issuable));
const result = await this.client.request<GqlProjectResult<GqlDiscussionsProject>>(query, { const result = await this.client.request<GqlProjectResult<GqlDiscussionsProject>>(query, {
projectPath, projectPath,
iid: String(issuable.iid), iid: String(issuable.iid),
...@@ -492,7 +511,6 @@ export class GitLabNewService { ...@@ -492,7 +511,6 @@ export class GitLabNewService {
assert(discussions.pageInfo.endCursor); assert(discussions.pageInfo.endCursor);
const remainingPages = await this.getDiscussions({ const remainingPages = await this.getDiscussions({
issuable, issuable,
includePosition,
endCursor: discussions.pageInfo.endCursor, endCursor: discussions.pageInfo.endCursor,
}); });
return [...discussions.nodes, ...remainingPages]; return [...discussions.nodes, ...remainingPages];
...@@ -541,12 +559,21 @@ export class GitLabNewService { ...@@ -541,12 +559,21 @@ export class GitLabNewService {
return combinedEvents; return combinedEvents;
} }
async createNote(issuable: RestIssuable, body: string, replyId?: string): Promise<void> { async createNote(issuable: RestIssuable, body: string, replyId?: string): Promise<GqlNote> {
await this.client.request<void>(createNoteMutation, { const result = await this.client.request<CreateNoteResult>(createNoteMutation, {
issuableId: getIssuableGqlId(issuable), issuableId: getIssuableGqlId(issuable),
body, body,
replyId, replyId,
}); });
if (result.createNote.errors.length > 0) {
throw new UserFriendlyError(
`Couldn't create the comment when calling the API.
For more information, review the extension logs.`,
new Error(result.createNote.errors.join(',')),
);
}
assert(result.createNote.note);
return result.createNote.note;
} }
async deleteNote(noteId: string): Promise<void> { async deleteNote(noteId: string): Promise<void> {
......
...@@ -81,10 +81,6 @@ describe('GitLabCommentThread', () => { ...@@ -81,10 +81,6 @@ describe('GitLabCommentThread', () => {
expect(vsCommentThread.collapsibleState).toBe(vscode.CommentThreadCollapsibleState.Expanded); expect(vsCommentThread.collapsibleState).toBe(vscode.CommentThreadCollapsibleState.Expanded);
}); });
it('sets canReply on the VS thread', () => {
expect(vsCommentThread.canReply).toBe(false);
});
it('takes position from the first note', () => { it('takes position from the first note', () => {
expect(vsCommentThread.range.start.line).toBe(noteOnDiff.position.oldLine - 1); // vs code numbers lines from 0 expect(vsCommentThread.range.start.line).toBe(noteOnDiff.position.oldLine - 1); // vs code numbers lines from 0
}); });
...@@ -95,6 +91,33 @@ describe('GitLabCommentThread', () => { ...@@ -95,6 +91,33 @@ describe('GitLabCommentThread', () => {
expect(vsCommentThread.uri.query).toMatch(baseSha); expect(vsCommentThread.uri.query).toMatch(baseSha);
}); });
describe('allowing replies to the thread', () => {
const createNoteAndSetCreatePermissions = (createNote: boolean): GqlTextDiffNote => ({
...(noteOnDiff as GqlTextDiffNote),
userPermissions: {
...noteOnDiff.userPermissions,
createNote,
},
});
it('disallows replies if the first note has createNote permissions', () => {
const note = createNoteAndSetCreatePermissions(true);
createGitLabCommentThread(createGqlTextDiffDiscussion(note));
// TODO: allow replies when finishing #339
expect(vsCommentThread.canReply).toBe(false);
});
it('disallows replies if the first note does not have createNote permissions', () => {
const note = createNoteAndSetCreatePermissions(false);
createGitLabCommentThread(createGqlTextDiffDiscussion(note));
expect(vsCommentThread.canReply).toBe(false);
});
});
describe('resolving discussions', () => { describe('resolving discussions', () => {
it('sets context to unresolved', () => { it('sets context to unresolved', () => {
expect(vsCommentThread.contextValue).toBe('unresolved'); expect(vsCommentThread.contextValue).toBe('unresolved');
......
...@@ -3,11 +3,18 @@ import * as assert from 'assert'; ...@@ -3,11 +3,18 @@ import * as assert from 'assert';
import { import {
GitLabNewService, GitLabNewService,
GqlTextDiffDiscussion, GqlTextDiffDiscussion,
GqlTextDiffNote,
GqlTextPosition, GqlTextPosition,
} from '../gitlab/gitlab_new_service'; } from '../gitlab/gitlab_new_service';
import { GitLabComment } from './gitlab_comment'; import { GitLabComment } from './gitlab_comment';
import { toReviewUri } from './review_uri'; import { toReviewUri } from './review_uri';
const firstNoteFrom = (discussion: GqlTextDiffDiscussion): GqlTextDiffNote => {
const note = discussion.notes.nodes[0];
assert(note, 'discussion should contain at least one note');
return note;
};
const commentRangeFromPosition = (position: GqlTextPosition): vscode.Range => { const commentRangeFromPosition = (position: GqlTextPosition): vscode.Range => {
const glLine = position.oldLine ?? position.newLine; const glLine = position.oldLine ?? position.newLine;
const vsPosition = new vscode.Position(glLine - 1, 0); // VS Code numbers lines starting with 0, GitLab starts with 1 const vsPosition = new vscode.Position(glLine - 1, 0); // VS Code numbers lines starting with 0, GitLab starts with 1
...@@ -48,6 +55,8 @@ export class GitLabCommentThread { ...@@ -48,6 +55,8 @@ export class GitLabCommentThread {
private mr: RestIssuable, private mr: RestIssuable,
) { ) {
this.vsThread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded; this.vsThread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
// TODO: when finishing #339, use the permissions to decide if replies should be allowed
// this.vsThread.canReply = firstNoteFrom(gqlDiscussion).userPermissions.createNote;
this.vsThread.canReply = false; this.vsThread.canReply = false;
this.resolved = gqlDiscussion.resolved; this.resolved = gqlDiscussion.resolved;
this.updateThreadContext(); this.updateThreadContext();
...@@ -125,7 +134,7 @@ export class GitLabCommentThread { ...@@ -125,7 +134,7 @@ export class GitLabCommentThread {
discussion, discussion,
gitlabService, gitlabService,
}: CreateThreadOptions): GitLabCommentThread { }: CreateThreadOptions): GitLabCommentThread {
const { position } = discussion.notes.nodes[0]; const { position } = firstNoteFrom(discussion);
const vsThread = commentController.createCommentThread( const vsThread = commentController.createCommentThread(
uriFromPosition(position, workspaceFolder, mr.project_id), uriFromPosition(position, workspaceFolder, mr.project_id),
commentRangeFromPosition(position), commentRangeFromPosition(position),
......
...@@ -40,6 +40,7 @@ const note1 = { ...@@ -40,6 +40,7 @@ const note1 = {
userPermissions: { userPermissions: {
resolveNote: true, resolveNote: true,
adminNote: true, adminNote: true,
createNote: true,
}, },
system: false, system: false,
author: { author: {
...@@ -64,6 +65,7 @@ const note2 = { ...@@ -64,6 +65,7 @@ const note2 = {
userPermissions: { userPermissions: {
resolveNote: true, resolveNote: true,
adminNote: true, adminNote: true,
createNote: true,
}, },
system: false, system: false,
author: { author: {
...@@ -87,6 +89,7 @@ const noteOnDiff = { ...@@ -87,6 +89,7 @@ const noteOnDiff = {
userPermissions: { userPermissions: {
resolveNote: true, resolveNote: true,
adminNote: true, adminNote: true,
createNote: true,
}, },
system: false, system: false,
author: { author: {
......
...@@ -6,7 +6,7 @@ const { graphql } = require('msw'); ...@@ -6,7 +6,7 @@ const { graphql } = require('msw');
const webviewController = require('../../src/webview_controller'); const webviewController = require('../../src/webview_controller');
const { tokenService } = require('../../src/services/token_service'); const { tokenService } = require('../../src/services/token_service');
const openIssueResponse = require('./fixtures/rest/open_issue.json'); const openIssueResponse = require('./fixtures/rest/open_issue.json');
const { projectWithIssueDiscussions } = require('./fixtures/graphql/discussions'); const { projectWithIssueDiscussions, note2 } = require('./fixtures/graphql/discussions');
const { getServer, createJsonEndpoint } = require('./test_infrastructure/mock_server'); const { getServer, createJsonEndpoint } = require('./test_infrastructure/mock_server');
const { GITLAB_URL } = require('./test_infrastructure/constants'); const { GITLAB_URL } = require('./test_infrastructure/constants');
...@@ -39,6 +39,7 @@ describe('GitLab webview', () => { ...@@ -39,6 +39,7 @@ describe('GitLab webview', () => {
ctx.data({ ctx.data({
createNote: { createNote: {
errors: [], errors: [],
note: note2,
}, },
}), }),
); );
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册