diff --git a/src/commands/insert_snippet.ts b/src/commands/insert_snippet.ts index ffb9e7d6493e02542c5f66672a93da0283b359eb..c11e9aee33bc0159558be887130e929691b6c672 100644 --- a/src/commands/insert_snippet.ts +++ b/src/commands/insert_snippet.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { GitLabNewService, GqlSnippet, GqlBlob } from '../gitlab/gitlab_new_service'; -import { createGitService } from '../git_service_factory'; +import { GqlSnippet, GqlBlob } from '../gitlab/gitlab_new_service'; +import { createGitService, createGitLabNewService } from '../service_factory'; import { getCurrentWorkspaceFolderOrSelectOne } from '../services/workspace_service'; const pickSnippet = async (snippets: GqlSnippet[]) => { @@ -32,8 +32,7 @@ export const insertSnippet = async (): Promise => { return; } const gitService = createGitService(workspaceFolder); - const instanceUrl = await gitService.fetchCurrentInstanceUrl(); - const gitLabService = new GitLabNewService(instanceUrl); + const gitLabService = await createGitLabNewService(workspaceFolder); const remote = await gitService.fetchGitRemote(); if (!remote) { throw new Error('Could not get parsed remote for your workspace'); diff --git a/src/data_providers/items/mr_item.ts b/src/data_providers/items/mr_item.ts index 8e96cbcc06ba8ee2ef6f3ef4d49e056856f28abc..fd73ea2d35da0855de797c8498ce5a365def0b10 100644 --- a/src/data_providers/items/mr_item.ts +++ b/src/data_providers/items/mr_item.ts @@ -1,7 +1,6 @@ import { TreeItem, TreeItemCollapsibleState, ThemeIcon, Uri } from 'vscode'; import { PROGRAMMATIC_COMMANDS } from '../../command_names'; -import { GitLabNewService } from '../../gitlab/gitlab_new_service'; -import { createGitService } from '../../git_service_factory'; +import { createGitLabNewService } from '../../service_factory'; import { ChangedFileItem } from './changed_file_item'; export class MrItem extends TreeItem { @@ -29,9 +28,7 @@ export class MrItem extends TreeItem { } private async getChangedFiles(): Promise { - const gitService = createGitService(this.project.uri); - const instanceUrl = await gitService.fetchCurrentInstanceUrl(); - const gitlabService = new GitLabNewService(instanceUrl); + const gitlabService = await createGitLabNewService(this.project.uri); const mrVersion = await gitlabService.getMrDiff(this.mr); return mrVersion.diffs.map(d => new ChangedFileItem(this.mr, mrVersion, d, this.project)); } diff --git a/src/gitlab/gitlab_new_service.ts b/src/gitlab/gitlab_new_service.ts index 268a38a54a4d99d2e876cb576267400a0bd455d6..a9fc17f47f6fe952cfb8a465d5ece41c80f7520f 100644 --- a/src/gitlab/gitlab_new_service.ts +++ b/src/gitlab/gitlab_new_service.ts @@ -52,7 +52,7 @@ interface GqlNote { body: string; // TODO: remove this once the SystemNote.vue doesn't require plain text body bodyHtml: string; } -export interface GqlDiscussion { +interface GqlDiscussion { replyId: string; createdAt: string; notes: Node; @@ -67,6 +67,19 @@ interface GqlDiscussionsProject { }; } +interface RestLabelEvent { + label: unknown; + body: string; + // eslint-disable-next-line camelcase + created_at: string; +} + +type Note = GqlDiscussion | RestLabelEvent; + +function isLabelEvent(note: Note): note is RestLabelEvent { + return (note as RestLabelEvent).label !== undefined; +} + const queryGetSnippets = gql` query GetSnippets($projectPath: ID!) { project(fullPath: $projectPath) { @@ -253,7 +266,10 @@ export class GitLabNewService { }; } - async getDiscussions(issuable: RestIssuable, endCursor?: string): Promise { + private async getDiscussions( + issuable: RestIssuable, + endCursor?: string, + ): Promise { const [projectPath] = issuable.references.full.split(/[#!]/); const query = issuable.sha ? queryGetMrDiscussions : queryGetIssueDiscussions; const result = await this.client.request>(query, { @@ -272,4 +288,30 @@ export class GitLabNewService { } return discussions.nodes.map(n => this.addHostToUrl(n)); } + + private async getLabelEvents(issuable: RestIssuable): Promise { + const type = issuable.sha ? 'merge_requests' : 'issues'; + const labelEventsUrl = `${this.instanceUrl}/api/v4/projects/${issuable.project_id}/${type}/${issuable.iid}/resource_label_events?sort=asc&per_page=100`; + const result = await crossFetch(labelEventsUrl, this.fetchOptions); + if (!result.ok) { + throw new FetchError(`Fetching file from ${labelEventsUrl} failed`, result); + } + return result.json(); + } + + async getDiscussionsAndLabelEvents(issuable: RestIssuable): Promise { + const [discussions, labelEvents] = await Promise.all([ + this.getDiscussions(issuable), + this.getLabelEvents(issuable), + ]); + + const combinedEvents: Note[] = [...discussions, ...labelEvents]; + combinedEvents.sort((a: Note, b: Note) => { + const aCreatedAt = isLabelEvent(a) ? a.created_at : a.createdAt; + const bCreatedAt = isLabelEvent(b) ? b.created_at : b.createdAt; + return aCreatedAt < bCreatedAt ? -1 : 1; + }); + + return combinedEvents; + } } diff --git a/src/gitlab_service.ts b/src/gitlab_service.ts index bf37214a45767cc7bcd26058e21f49e1be627b9b..c5eef1fed5dfc4a737c4ae8d16ee2b0173cae42f 100644 --- a/src/gitlab_service.ts +++ b/src/gitlab_service.ts @@ -5,13 +5,12 @@ import { tokenService } from './services/token_service'; import { UserFriendlyError } from './errors/user_friendly_error'; import { ApiError } from './errors/api_error'; import { getCurrentWorkspaceFolder } from './services/workspace_service'; -import { createGitService } from './git_service_factory'; +import { createGitService } from './service_factory'; import { GitRemote } from './git/git_remote_parser'; import { handleError, logError } from './log'; import { getUserAgentHeader } from './utils/get_user_agent_header'; import { CustomQueryType } from './gitlab/custom_query_type'; import { CustomQuery } from './gitlab/custom_query'; -import { GitLabNewService, GqlDiscussion } from './gitlab/gitlab_new_service'; interface GitLabProject { id: number; @@ -482,35 +481,6 @@ export async function validateCIConfig(content: string) { return validCIConfig; } - -interface LabelEvent { - label: unknown; - body: string; - // eslint-disable-next-line camelcase - created_at: string; -} - -export async function fetchLabelEvents(issuable: RestIssuable): Promise { - let labelEvents: LabelEvent[] = []; - - try { - const type = issuable.sha ? 'merge_requests' : 'issues'; - const { response } = await fetch( - `/projects/${issuable.project_id}/${type}/${issuable.iid}/resource_label_events?sort=asc&per_page=100`, - ); - labelEvents = response; - } catch (e) { - handleError(new UserFriendlyError('Failed to fetch label events for this issuable.', e)); - } - - labelEvents.forEach(el => { - // Temporarily disable eslint to be able to start enforcing stricter rules - // eslint-disable-next-line no-param-reassign - el.body = ''; - }); - return labelEvents; -} - interface Discussion { notes: { // eslint-disable-next-line camelcase @@ -566,30 +536,3 @@ export async function saveNote(params: { return { success: false }; } - -type note = GqlDiscussion | LabelEvent; - -function isLabelEvent(object: any): object is LabelEvent { - return Boolean(object.label); -} - -export async function fetchDiscussionsAndLabelEvents(issuable: RestIssuable): Promise { - // obtaining GitLabNewService in GitLabService is temporary and will be removed in this or the next MR - const instanceUrl = await createGitService( - (await getCurrentWorkspaceFolder()) || '', - ).fetchCurrentInstanceUrl(); - const gitlabNewService = new GitLabNewService(instanceUrl); - const [discussions, labelEvents] = await Promise.all([ - gitlabNewService.getDiscussions(issuable), - fetchLabelEvents(issuable), - ]); - - const combinedEvents: note[] = [...discussions, ...labelEvents]; - combinedEvents.sort((a: note, b: note) => { - const aCreatedAt = isLabelEvent(a) ? a.created_at : a.createdAt; - const bCreatedAt = isLabelEvent(b) ? b.created_at : b.createdAt; - return aCreatedAt < bCreatedAt ? -1 : 1; - }); - - return combinedEvents; -} diff --git a/src/openers.js b/src/openers.js index 98164b69666becd5762484119f2784a2ba4c809e..2de063740468a5c87b9bbfe9546fb9e1f52bb4a2 100644 --- a/src/openers.js +++ b/src/openers.js @@ -1,7 +1,7 @@ const vscode = require('vscode'); const gitLabService = require('./gitlab_service'); const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service'); -const { createGitService } = require('./git_service_factory'); +const { createGitService } = require('./service_factory'); const { handleError } = require('./log'); const { VS_COMMANDS } = require('./command_names'); diff --git a/src/review/api_content_provider.ts b/src/review/api_content_provider.ts index ca43ba15b77e7c92d660bf6ce9eb6291162a8237..ec5bb392f0ee5bb035fef9e9070b4189d2bb8794 100644 --- a/src/review/api_content_provider.ts +++ b/src/review/api_content_provider.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode'; import { fromReviewUri } from './review_uri'; -import { GitLabNewService } from '../gitlab/gitlab_new_service'; import { logError } from '../log'; -import { createGitService } from '../git_service_factory'; +import { createGitLabNewService } from '../service_factory'; export class ApiContentProvider implements vscode.TextDocumentContentProvider { // eslint-disable-next-line class-methods-use-this @@ -13,8 +12,7 @@ export class ApiContentProvider implements vscode.TextDocumentContentProvider { const params = fromReviewUri(uri); if (!params.path || !params.commit) return ''; - const instanceUrl = await createGitService(params.workspacePath).fetchCurrentInstanceUrl(); - const service = new GitLabNewService(instanceUrl); + const service = await createGitLabNewService(params.workspacePath); try { return service.getFileContent(params.path, params.commit, params.projectId); } catch (e) { diff --git a/src/search_input.js b/src/search_input.js index d366d5bf83e06741ba4ee207808885daa70eb84e..8beb5744187eeac0afcbc877c5086e6e452e48b2 100644 --- a/src/search_input.js +++ b/src/search_input.js @@ -2,7 +2,7 @@ const vscode = require('vscode'); const gitLabService = require('./gitlab_service'); const openers = require('./openers'); const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service'); -const { createGitService } = require('./git_service_factory'); +const { createGitService } = require('./service_factory'); const parseQuery = (query, noteableType) => { const params = {}; diff --git a/src/git_service_factory.ts b/src/service_factory.ts similarity index 71% rename from src/git_service_factory.ts rename to src/service_factory.ts index 77bec93aa676f5f45dc76023fb1527a1ddde638f..dac904f6770a2e4aafa32d99868f466c21fc414e 100644 --- a/src/git_service_factory.ts +++ b/src/service_factory.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { GitService } from './git_service'; import { tokenService } from './services/token_service'; import { log } from './log'; +import { GitLabNewService } from './gitlab/gitlab_new_service'; export function createGitService(workspaceFolder: string): GitService { const { instanceUrl, remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration( @@ -18,3 +19,8 @@ export function createGitService(workspaceFolder: string): GitService { log, }); } + +export async function createGitLabNewService(workspaceFolder: string): Promise { + const gitService = createGitService(workspaceFolder); + return new GitLabNewService(await gitService.fetchCurrentInstanceUrl()); +} diff --git a/src/webview/src/components/LabelNote.test.js b/src/webview/src/components/LabelNote.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a22596315cc206e0cfda378ca2f28ceecb0e2984 --- /dev/null +++ b/src/webview/src/components/LabelNote.test.js @@ -0,0 +1,80 @@ +import { mount } from '@vue/test-utils'; +import LabelNote from './LabelNote'; + +const addedLabelEvent = require('../../../../test/integration/fixtures/rest/label_events/added_normal.json'); +const scopedLabelEvent = require('../../../../test/integration/fixtures/rest/label_events/added_scoped.json'); + +describe('LabelNote', () => { + let wrapper; + const tooltipDirective = jest.fn(); + + beforeEach(() => { + tooltipDirective.mockReset(); + window.vsCodeApi = { postMessage: jest.fn() }; + }); + describe('added normal event', () => { + beforeEach(() => { + wrapper = mount(LabelNote, { + propsData: { + noteable: addedLabelEvent, + }, + stubs: { + date: true, + }, + directives: { + tooltip: tooltipDirective, + }, + }); + }); + it('contains correct text', () => { + expect(wrapper.text()).toBe('Tomas Vik @viktomas added Category:Editor Extension label ·'); + }); + + describe('label pill', () => { + it('has correct colors', () => { + expect(wrapper.find('.label-pill').attributes().style).toBe( + 'background-color: rgb(66, 139, 202); color: rgb(255, 255, 255); border-color: #428bca;', + ); + }); + + it('has correct tooltip', () => { + expect(tooltipDirective.mock.calls[0][1].value).toBe( + 'Issues related to the Editor Extension category: https://about.gitlab.com/direction/create/editor_extension/', + ); + }); + }); + }); + + describe('added scoped event', () => { + beforeEach(() => { + wrapper = mount(LabelNote, { + propsData: { + noteable: scopedLabelEvent, + }, + stubs: { + date: true, + }, + directives: { + tooltip: tooltipDirective, + }, + }); + }); + it('contains correct text', () => { + expect(wrapper.text()).toBe('Tomas Vik @viktomas added group code review label ·'); + }); + + describe('label pill', () => { + it('has correct colors', () => { + expect(wrapper.find('.label-pill').attributes().style).toBe( + 'background-color: rgb(168, 214, 149); color: rgb(51, 51, 51); border-color: #a8d695;', + ); + }); + + it('has scoped label with inverted colors inside', () => { + expect(wrapper.find('.scoped-pill').attributes().style).toBe( + 'background-color: rgb(51, 51, 51); color: rgb(168, 214, 149);', + ); + }); + }); + }); +}); diff --git a/src/webview/src/components/LabelNote.vue b/src/webview/src/components/LabelNote.vue index 1dc3482bbdbbc90340c44316244cb9cb5b9de07f..603d8069c29126af7dbea234739ad7fd6a36c9f8 100644 --- a/src/webview/src/components/LabelNote.vue +++ b/src/webview/src/components/LabelNote.vue @@ -1,5 +1,4 @@ @@ -44,9 +42,30 @@ export default {
- - · - + + {{ action }} + + {{ labelName }} + {{ scopedName }} + + label · +
@@ -92,6 +111,34 @@ export default { } } + .note-header { + align-items: baseline; + .label-action, + .label-divider, + .label-pill { + margin-right: 3px; + } + } + + .label-pill { + line-height: 1rem; + font-size: 0.75rem; + border-radius: 0.6rem; // slightly larger than the line-height because of the 1px border + border-width: 1px; + border-style: solid; + display: flex; + .label-name, + .scoped-pill { + padding: 0 3px; + } + + .scoped-pill { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + height: 100%; + } + } + ul { list-style-type: disc; padding-inline-start: 16px; diff --git a/src/webview_controller.js b/src/webview_controller.js index 05446eda3b496cc0300587df39ea94186ba6b367..8787af6cda3c3a4dd1a3a8dfb8af9de2a6b9e182 100644 --- a/src/webview_controller.js +++ b/src/webview_controller.js @@ -3,6 +3,7 @@ const path = require('path'); const crypto = require('crypto'); const vscode = require('vscode'); const gitLabService = require('./gitlab_service'); +const { createGitLabNewService } = require('./service_factory'); let context = null; @@ -84,8 +85,13 @@ const createMessageHandler = (panel, issuable, workspaceFolder) => async message }); if (response.success !== false) { - const discussions = await gitLabService.fetchDiscussionsAndLabelEvents(issuable); - panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions }); + const gitlabNewService = await createGitLabNewService(workspaceFolder); + const discussionsAndLabels = await gitlabNewService.getDiscussionsAndLabelEvents(issuable); + panel.webview.postMessage({ + type: 'issuableFetch', + issuable, + discussions: discussionsAndLabels, + }); panel.webview.postMessage({ type: 'noteSaved' }); } else { panel.webview.postMessage({ type: 'noteSaved', status: false }); @@ -93,7 +99,7 @@ const createMessageHandler = (panel, issuable, workspaceFolder) => async message } }; -async function handleChangeViewState(panel, issuable) { +async function handleChangeViewState(panel, issuable, workspaceFolder) { if (!panel.active) return; const appReadyPromise = new Promise(resolve => { @@ -105,9 +111,10 @@ async function handleChangeViewState(panel, issuable) { }); }); - const discussonsAndLabels = await gitLabService.fetchDiscussionsAndLabelEvents(issuable); + const gitlabNewService = await createGitLabNewService(workspaceFolder); + const discussionsAndLabels = await gitlabNewService.getDiscussionsAndLabelEvents(issuable); await appReadyPromise; - panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions: discussonsAndLabels }); + panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions: discussionsAndLabels }); } const getIconPathForIssuable = issuable => { @@ -130,7 +137,7 @@ async function create(issuable, workspaceFolder) { panel.iconPath = getIconPathForIssuable(issuable); panel.onDidChangeViewState(() => { - handleChangeViewState(panel, issuable); + handleChangeViewState(panel, issuable, workspaceFolder); }); panel.webview.onDidReceiveMessage(createMessageHandler(panel, issuable, workspaceFolder)); diff --git a/test/integration/fixtures/rest/label_events/added_normal.json b/test/integration/fixtures/rest/label_events/added_normal.json new file mode 100644 index 0000000000000000000000000000000000000000..da58e8038c8911d2a4f09199b06ebc110f50797b --- /dev/null +++ b/test/integration/fixtures/rest/label_events/added_normal.json @@ -0,0 +1,23 @@ +{ + "id": 77400724, + "user": { + "id": 3457201, + "name": "Tomas Vik", + "username": "viktomas", + "state": "active", + "avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon", + "web_url": "https://gitlab.com/viktomas" + }, + "created_at": "2020-12-11T13:13:20.192Z", + "resource_type": "MergeRequest", + "resource_id": 81385331, + "label": { + "id": 15126283, + "name": "Category:Editor Extension", + "color": "#428BCA", + "description": "Issues related to the Editor Extension category: https://about.gitlab.com/direction/create/editor_extension/", + "description_html": "Issues related to the Editor Extension category: https://about.gitlab.com/direction/create/editor_extension/", + "text_color": "#FFFFFF" + }, + "action": "add" +} diff --git a/test/integration/fixtures/rest/label_events/added_scoped.json b/test/integration/fixtures/rest/label_events/added_scoped.json new file mode 100644 index 0000000000000000000000000000000000000000..3ec862b4bbc6f4019915fce1ac2aa44f8ccf1808 --- /dev/null +++ b/test/integration/fixtures/rest/label_events/added_scoped.json @@ -0,0 +1,23 @@ +{ + "id": 77400728, + "user": { + "id": 3457201, + "name": "Tomas Vik", + "username": "viktomas", + "state": "active", + "avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon", + "web_url": "https://gitlab.com/viktomas" + }, + "created_at": "2020-12-11T13:13:20.192Z", + "resource_type": "MergeRequest", + "resource_id": 81385331, + "label": { + "id": 16934793, + "name": "group::code review", + "color": "#A8D695", + "description": "Issues belonging to the Code Review group of the Create stage of the DevOps lifecycle.", + "description_html": "Issues belonging to the Code Review group of the Create stage of the DevOps lifecycle.", + "text_color": "#333333" + }, + "action": "add" +} diff --git a/test/integration/fixtures/rest/label_events/removed_normal.json b/test/integration/fixtures/rest/label_events/removed_normal.json new file mode 100644 index 0000000000000000000000000000000000000000..929b2d1b3b12d132867a5495c299fa92bed3e2fb --- /dev/null +++ b/test/integration/fixtures/rest/label_events/removed_normal.json @@ -0,0 +1,23 @@ +{ + "id": 77400845, + "user": { + "id": 3457201, + "name": "Tomas Vik", + "username": "viktomas", + "state": "active", + "avatar_url": "https://secure.gravatar.com/avatar/6042a9152ada74d9fb6a0cdce895337e?s=80&d=identicon", + "web_url": "https://gitlab.com/viktomas" + }, + "created_at": "2020-12-11T13:14:03.000Z", + "resource_type": "MergeRequest", + "resource_id": 81385331, + "label": { + "id": 1890178, + "name": "Accepting merge requests", + "color": "#69D100", + "description": "Issues opened for contribution from the Community. Issue's weight is an estimation of complexity. Please mention @gitlab-org/coaches if you have any questions :)", + "description_html": "Issues opened for contribution from the Community. Issue's weight is an estimation of complexity. Please mention @gitlab-org/coaches if you have any questions :)", + "text_color": "#FFFFFF" + }, + "action": "remove" +}