Skip to content

Commit

Permalink
feat: Add APIs for linear integration (#9346)
Browse files Browse the repository at this point in the history
  • Loading branch information
muhsin-k committed May 22, 2024
1 parent 0d13c11 commit 023b3ad
Show file tree
Hide file tree
Showing 16 changed files with 1,307 additions and 23 deletions.
93 changes: 93 additions & 0 deletions app/controllers/api/v1/accounts/integrations/linear_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:link_issue, :linked_issues]

def teams
teams = linear_processor_service.teams
if teams[:error]
render json: { error: teams[:error] }, status: :unprocessable_entity
else
render json: teams[:data], status: :ok
end
end

def team_entities
team_id = permitted_params[:team_id]
team_entities = linear_processor_service.team_entities(team_id)
if team_entities[:error]
render json: { error: team_entities[:error] }, status: :unprocessable_entity
else
render json: team_entities[:data], status: :ok
end
end

def create_issue
issue = linear_processor_service.create_issue(permitted_params)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end

def link_issue
issue_id = permitted_params[:issue_id]
title = permitted_params[:title]
issue = linear_processor_service.link_issue(conversation_link, issue_id, title)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end

def unlink_issue
link_id = permitted_params[:link_id]
issue = linear_processor_service.unlink_issue(link_id)

if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end

def linked_issues
issues = linear_processor_service.linked_issues(conversation_link)

if issues[:error]
render json: { error: issues[:error] }, status: :unprocessable_entity
else
render json: issues[:data], status: :ok
end
end

def search_issue
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return

term = params[:q]
issues = linear_processor_service.search_issue(term)
if issues[:error]
render json: { error: issues[:error] }, status: :unprocessable_entity
else
render json: issues[:data], status: :ok
end
end

private

def conversation_link
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/conversations/#{@conversation.display_id}"
end

def fetch_conversation
@conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id])
end

def linear_processor_service
Integrations::Linear::ProcessorService.new(account: Current.account)
end

def permitted_params
params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
end
end
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<template>
<div class="flex-shrink flex-grow overflow-auto p-4">
<div class="flex-grow flex-shrink p-4 overflow-auto">
<div class="flex flex-col">
<div v-if="uiFlags.isFetching" class="my-0 mx-auto">
<div v-if="uiFlags.isFetching" class="mx-auto my-0">
<woot-loading-state :message="$t('INTEGRATION_APPS.FETCHING')" />
</div>

<div v-else class="w-full">
<div>
<div
v-for="item in integrationsList"
v-for="item in enabledIntegrations"
:key="item.id"
class="bg-white dark:bg-slate-800 border border-solid border-slate-75 dark:border-slate-700/50 rounded-sm mb-4 p-4"
class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
>
<integration-item
:integration-id="item.id"
Expand All @@ -25,22 +25,38 @@
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';

<script setup>
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { computed, onMounted } from 'vue';
import IntegrationItem from './IntegrationItem.vue';
const store = useStore();
const getters = useStoreGetters();
const uiFlags = getters['integrations/getUIFlags'];
const accountId = getters.getCurrentAccountId;
const integrationList = computed(() => {
return getters['integrations/getAppIntegrations'].value;
});
const isLinearIntegrationEnabled = computed(() => {
return getters['accounts/isFeatureEnabledonAccount'].value(
accountId.value,
'linear_integration'
);
});
const enabledIntegrations = computed(() => {
if (!isLinearIntegrationEnabled.value) {
return integrationList.value.filter(
integration => integration.id !== 'linear'
);
}
return integrationList.value;
});
export default {
components: {
IntegrationItem,
},
computed: {
...mapGetters({
uiFlags: 'labels/getUIFlags',
integrationsList: 'integrations/getAppIntegrations',
}),
},
mounted() {
this.$store.dispatch('integrations/get');
},
};
onMounted(() => {
store.dispatch('integrations/get');
});
</script>
10 changes: 7 additions & 3 deletions app/javascript/dashboard/store/modules/integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ const state = {
};

const isAValidAppIntegration = integration => {
return ['dialogflow', 'dyte', 'google_translate', 'openai'].includes(
integration.id
);
return [
'dialogflow',
'dyte',
'google_translate',
'openai',
'linear',
].includes(integration.id);
};
export const getters = {
getIntegrations($state) {
Expand Down
2 changes: 2 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@
- name: help_center_embedding_search
enabled: false
premium: true
- name: linear_integration
enabled: false
24 changes: 24 additions & 0 deletions config/integration/apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,27 @@ openai:
},
]
visible_properties: ['api_key', 'label_suggestion']
linear:
id: linear
logo: linear.png
i18n_key: linear
action: /linear
hook_type: account
allow_multiple_hooks: false
settings_json_schema: {
"type": "object",
"properties": {
"api_key": { "type": "string" },
},
"required": ["api_key"],
"additionalProperties": false,
}
settings_form_schema: [
{
"label": "API Key",
"type": "text",
"name": "api_key",
"validation": "required",
},
]
visible_properties: []
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ en:
openai:
name: "OpenAI"
description: "Integrate powerful AI features into Chatwoot by leveraging the GPT models from OpenAI."
linear:
name: "Linear"
description: "Create Linear issues from conversations, or link existing ones for seamless tracking."
public_portal:
search:
search_placeholder: Search for article by title or body...
Expand Down
11 changes: 11 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@
post :add_participant_to_meeting
end
end
resource :linear, controller: 'linear', only: [] do
collection do
get :teams
get :team_entities
post :create_issue
post :link_issue
post :unlink_issue
get :search_issue
get :linked_issues
end
end
end
resources :working_hours, only: [:update]

Expand Down
82 changes: 82 additions & 0 deletions lib/integrations/linear/processor_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
class Integrations::Linear::ProcessorService
pattr_initialize [:account!]

def teams
response = linear_client.teams
return { error: response[:error] } if response[:error]

{ data: response['teams']['nodes'].map(&:as_json) }
end

def team_entities(team_id)
response = linear_client.team_entities(team_id)
return response if response[:error]

{
data: {
users: response['users']['nodes'].map(&:as_json),
projects: response['projects']['nodes'].map(&:as_json),
states: response['workflowStates']['nodes'].map(&:as_json),
labels: response['issueLabels']['nodes'].map(&:as_json)
}
}
end

def create_issue(params)
response = linear_client.create_issue(params)
return response if response[:error]

{
data: { id: response['issueCreate']['issue']['id'],
title: response['issueCreate']['issue']['title'] }
}
end

def link_issue(link, issue_id, title)
response = linear_client.link_issue(link, issue_id, title)
return response if response[:error]

{
data: {
id: issue_id,
link: link,
link_id: response.with_indifferent_access[:attachmentLinkURL][:attachment][:id]
}
}
end

def unlink_issue(link_id)
response = linear_client.unlink_issue(link_id)
return response if response[:error]

{
data: { link_id: link_id }
}
end

def search_issue(term)
response = linear_client.search_issue(term)

return response if response[:error]

{ data: response['searchIssues']['nodes'].map(&:as_json) }
end

def linked_issues(url)
response = linear_client.linked_issues(url)
return response if response[:error]

{ data: response['attachmentsForURL']['nodes'].map(&:as_json) }
end

private

def linear_hook
@linear_hook ||= account.hooks.find_by!(app_id: 'linear')
end

def linear_client
credentials = linear_hook.settings
@linear_client ||= Linear.new(credentials['api_key'])
end
end

0 comments on commit 023b3ad

Please sign in to comment.