diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb index 9d5d76d75..814373c7e 100644 --- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -88,6 +88,6 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas end def permitted_params - params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) + params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) end end diff --git a/app/javascript/dashboard/api/integrations/linear.js b/app/javascript/dashboard/api/integrations/linear.js new file mode 100644 index 000000000..600f169d6 --- /dev/null +++ b/app/javascript/dashboard/api/integrations/linear.js @@ -0,0 +1,46 @@ +/* global axios */ + +import ApiClient from '../ApiClient'; + +class LinearAPI extends ApiClient { + constructor() { + super('integrations/linear', { accountScoped: true }); + } + + getTeams() { + return axios.get(`${this.url}/teams`); + } + + getTeamEntities(teamId) { + return axios.get(`${this.url}/team_entities?team_id=${teamId}`); + } + + createIssue(data) { + return axios.post(`${this.url}/create_issue`, data); + } + + link_issue(conversationId, issueId) { + return axios.post(`${this.url}/link_issue`, { + issue_id: issueId, + conversation_id: conversationId, + }); + } + + getLinkedIssue(conversationId) { + return axios.get( + `${this.url}/linked_issues?conversation_id=${conversationId}` + ); + } + + unlinkIssue(linkId) { + return axios.post(`${this.url}/unlink_issue`, { + link_id: linkId, + }); + } + + searchIssues(query) { + return axios.get(`${this.url}/search_issue?q=${query}`); + } +} + +export default new LinearAPI(); diff --git a/app/javascript/dashboard/api/specs/integrations/linear.spec.js b/app/javascript/dashboard/api/specs/integrations/linear.spec.js new file mode 100644 index 000000000..cc16feb16 --- /dev/null +++ b/app/javascript/dashboard/api/specs/integrations/linear.spec.js @@ -0,0 +1,202 @@ +import LinearAPIClient from '../../integrations/linear'; +import ApiClient from '../../ApiClient'; + +describe('#linearAPI', () => { + it('creates correct instance', () => { + expect(LinearAPIClient).toBeInstanceOf(ApiClient); + expect(LinearAPIClient).toHaveProperty('getTeams'); + expect(LinearAPIClient).toHaveProperty('getTeamEntities'); + expect(LinearAPIClient).toHaveProperty('createIssue'); + expect(LinearAPIClient).toHaveProperty('link_issue'); + expect(LinearAPIClient).toHaveProperty('getLinkedIssue'); + expect(LinearAPIClient).toHaveProperty('unlinkIssue'); + expect(LinearAPIClient).toHaveProperty('searchIssues'); + }); + + describe('getTeams', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.getTeams(); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/integrations/linear/teams' + ); + }); + }); + + describe('getTeamEntities', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.getTeamEntities(1); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/integrations/linear/team_entities?team_id=1' + ); + }); + }); + + describe('createIssue', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + const issueData = { + title: 'New Issue', + description: 'Issue description', + }; + LinearAPIClient.createIssue(issueData); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/create_issue', + issueData + ); + }); + }); + + describe('link_issue', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.link_issue(1, 2); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/link_issue', + { + issue_id: 2, + conversation_id: 1, + } + ); + }); + }); + + describe('getLinkedIssue', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.getLinkedIssue(1); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/integrations/linear/linked_issues?conversation_id=1' + ); + }); + }); + + describe('unlinkIssue', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.unlinkIssue(1); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/unlink_issue', + { + link_id: 1, + } + ); + }); + }); + + describe('searchIssues', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('creates a valid request', () => { + LinearAPIClient.searchIssues('query'); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/integrations/linear/search_issue?q=query' + ); + }); + }); +}); diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index c56f27e14..91423c762 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -1,5 +1,5 @@