mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	chore: Linear integration fixes (#9538)
This commit is contained in:
		| @@ -99,8 +99,10 @@ export default { | |||||||
|     onMouseUp() { |     onMouseUp() { | ||||||
|       if (this.mousedDownOnBackdrop) { |       if (this.mousedDownOnBackdrop) { | ||||||
|         this.mousedDownOnBackdrop = false; |         this.mousedDownOnBackdrop = false; | ||||||
|  |         if (this.closeOnBackdropClick) { | ||||||
|           this.onClose(); |           this.onClose(); | ||||||
|         } |         } | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| <script setup> | <script setup> | ||||||
| import { ref, computed } from 'vue'; | import { ref, computed } from 'vue'; | ||||||
|  | import { debounce } from '@chatwoot/utils'; | ||||||
| import { picoSearch } from '@scmmishra/pico-search'; | import { picoSearch } from '@scmmishra/pico-search'; | ||||||
| import ListItemButton from './DropdownListItemButton.vue'; | import ListItemButton from './DropdownListItemButton.vue'; | ||||||
| import DropdownSearch from './DropdownSearch.vue'; | import DropdownSearch from './DropdownSearch.vue'; | ||||||
| import DropdownEmptyState from './DropdownEmptyState.vue'; | import DropdownEmptyState from './DropdownEmptyState.vue'; | ||||||
|  | import DropdownLoadingState from './DropdownLoadingState.vue'; | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   listItems: { |   listItems: { | ||||||
| @@ -26,16 +28,24 @@ const props = defineProps({ | |||||||
|     type: Boolean, |     type: Boolean, | ||||||
|     default: false, |     default: false, | ||||||
|   }, |   }, | ||||||
|  |   isLoading: { | ||||||
|  |     type: Boolean, | ||||||
|  |     default: false, | ||||||
|  |   }, | ||||||
|  |   loadingPlaceholder: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const emits = defineEmits(['on-search']); | const emits = defineEmits(['on-search']); | ||||||
|  |  | ||||||
| const searchTerm = ref(''); | const searchTerm = ref(''); | ||||||
|  |  | ||||||
| const onSearch = value => { | const onSearch = debounce(value => { | ||||||
|   searchTerm.value = value; |   searchTerm.value = value; | ||||||
|   emits('on-search', value); |   emits('on-search', value); | ||||||
| }; | }, 300); | ||||||
|  |  | ||||||
| const filteredListItems = computed(() => { | const filteredListItems = computed(() => { | ||||||
|   if (!searchTerm.value) return props.listItems; |   if (!searchTerm.value) return props.listItems; | ||||||
| @@ -50,6 +60,16 @@ const isFilterActive = id => { | |||||||
|   if (!props.activeFilterId) return false; |   if (!props.activeFilterId) return false; | ||||||
|   return id === props.activeFilterId; |   return id === props.activeFilterId; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const shouldShowLoadingState = computed(() => { | ||||||
|  |   return ( | ||||||
|  |     props.isLoading && isDropdownListEmpty.value && props.loadingPlaceholder | ||||||
|  |   ); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const shouldShowEmptyState = computed(() => { | ||||||
|  |   return !props.isLoading && isDropdownListEmpty.value; | ||||||
|  | }); | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
| @@ -67,8 +87,12 @@ const isFilterActive = id => { | |||||||
|       /> |       /> | ||||||
|     </slot> |     </slot> | ||||||
|     <slot name="listItem"> |     <slot name="listItem"> | ||||||
|  |       <dropdown-loading-state | ||||||
|  |         v-if="shouldShowLoadingState" | ||||||
|  |         :message="loadingPlaceholder" | ||||||
|  |       /> | ||||||
|       <dropdown-empty-state |       <dropdown-empty-state | ||||||
|         v-if="isDropdownListEmpty" |         v-else-if="shouldShowEmptyState" | ||||||
|         :message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')" |         :message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')" | ||||||
|       /> |       /> | ||||||
|       <list-item-button |       <list-item-button | ||||||
|   | |||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | <script setup> | ||||||
|  | defineProps({ | ||||||
|  |   message: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300" | ||||||
|  |   > | ||||||
|  |     {{ message }} | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @@ -39,7 +39,7 @@ const toggleShowAllNRT = () => { | |||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="absolute flex flex-col items-start bg-[#fdfdfd] dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto" |     class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto" | ||||||
|   > |   > | ||||||
|     <span class="text-sm font-medium text-slate-900 dark:text-slate-25"> |     <span class="text-sm font-medium text-slate-900 dark:text-slate-25"> | ||||||
|       {{ $t('SLA.EVENTS.TITLE') }} |       {{ $t('SLA.EVENTS.TITLE') }} | ||||||
|   | |||||||
| @@ -107,6 +107,7 @@ import { useI18n } from 'dashboard/composables/useI18n'; | |||||||
| import { useAlert } from 'dashboard/composables'; | import { useAlert } from 'dashboard/composables'; | ||||||
| import LinearAPI from 'dashboard/api/integrations/linear'; | import LinearAPI from 'dashboard/api/integrations/linear'; | ||||||
| import validations from './validations'; | import validations from './validations'; | ||||||
|  | import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   accountId: { |   accountId: { | ||||||
| @@ -140,6 +141,14 @@ const priorities = [ | |||||||
|   { id: 4, name: 'Low' }, |   { id: 4, name: 'Low' }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | const statusDesiredOrder = [ | ||||||
|  |   'Backlog', | ||||||
|  |   'Todo', | ||||||
|  |   'In Progress', | ||||||
|  |   'Done', | ||||||
|  |   'Canceled', | ||||||
|  | ]; | ||||||
|  |  | ||||||
| const isCreating = ref(false); | const isCreating = ref(false); | ||||||
| const inputStyles = { borderRadius: '12px', fontSize: '14px' }; | const inputStyles = { borderRadius: '12px', fontSize: '14px' }; | ||||||
|  |  | ||||||
| @@ -177,7 +186,11 @@ const getTeams = async () => { | |||||||
|     const response = await LinearAPI.getTeams(); |     const response = await LinearAPI.getTeams(); | ||||||
|     teams.value = response.data; |     teams.value = response.data; | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -186,12 +199,16 @@ const getTeamEntities = async () => { | |||||||
|     const response = await LinearAPI.getTeamEntities(formState.teamId); |     const response = await LinearAPI.getTeamEntities(formState.teamId); | ||||||
|     assignees.value = response.data.users; |     assignees.value = response.data.users; | ||||||
|     labels.value = response.data.labels; |     labels.value = response.data.labels; | ||||||
|     statuses.value = response.data.states; |  | ||||||
|     projects.value = response.data.projects; |     projects.value = response.data.projects; | ||||||
|  |     statuses.value = statusDesiredOrder | ||||||
|  |       .map(name => response.data.states.find(status => status.name === name)) | ||||||
|  |       .filter(Boolean); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert( |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|       t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ENTITIES_ERROR') |       t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ENTITIES_ERROR') | ||||||
|     ); |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -226,7 +243,11 @@ const createIssue = async () => { | |||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS')); |     useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS')); | ||||||
|     onClose(); |     onClose(); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } finally { |   } finally { | ||||||
|     isCreating.value = false; |     isCreating.value = false; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ const unlinkIssue = () => { | |||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="absolute flex flex-col items-start bg-ash-50 dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto" |     class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto" | ||||||
|   > |   > | ||||||
|     <div class="flex flex-col w-full"> |     <div class="flex flex-col w-full"> | ||||||
|       <issue-header |       <issue-header | ||||||
|   | |||||||
| @@ -16,9 +16,16 @@ | |||||||
|         variant="clear" |         variant="clear" | ||||||
|         color-scheme="secondary" |         color-scheme="secondary" | ||||||
|         class="h-[24px]" |         class="h-[24px]" | ||||||
|  |         :is-loading="isUnlinking" | ||||||
|         @click="unlinkIssue" |         @click="unlinkIssue" | ||||||
|       > |       > | ||||||
|         <fluent-icon icon="unlink" size="12" type="outline" icon-lib="lucide" /> |         <fluent-icon | ||||||
|  |           v-if="!isUnlinking" | ||||||
|  |           icon="unlink" | ||||||
|  |           size="12" | ||||||
|  |           type="outline" | ||||||
|  |           icon-lib="lucide" | ||||||
|  |         /> | ||||||
|       </woot-button> |       </woot-button> | ||||||
|       <woot-button |       <woot-button | ||||||
|         variant="clear" |         variant="clear" | ||||||
| @@ -33,6 +40,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
|  | import { inject } from 'vue'; | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   identifier: { |   identifier: { | ||||||
|     type: String, |     type: String, | ||||||
| @@ -44,6 +52,8 @@ const props = defineProps({ | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | const isUnlinking = inject('isUnlinking'); | ||||||
|  |  | ||||||
| const emit = defineEmits(['unlink-issue']); | const emit = defineEmits(['unlink-issue']); | ||||||
|  |  | ||||||
| const unlinkIssue = () => { | const unlinkIssue = () => { | ||||||
|   | |||||||
| @@ -16,9 +16,11 @@ | |||||||
|           :show-clear-filter="false" |           :show-clear-filter="false" | ||||||
|           :list-items="issues" |           :list-items="issues" | ||||||
|           :active-filter-id="selectedOption.id" |           :active-filter-id="selectedOption.id" | ||||||
|  |           :is-loading="isFetching" | ||||||
|           :input-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SEARCH')" |           :input-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SEARCH')" | ||||||
|  |           :loading-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.LOADING')" | ||||||
|           enable-search |           enable-search | ||||||
|           class="left-0 flex flex-col w-full overflow-y-auto h-fit max-h-[160px] md:left-auto md:right-0 top-10" |           class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10" | ||||||
|           @on-search="onSearch" |           @on-search="onSearch" | ||||||
|           @click="onSelectIssue" |           @click="onSelectIssue" | ||||||
|         /> |         /> | ||||||
| @@ -50,6 +52,7 @@ import { useAlert } from 'dashboard/composables'; | |||||||
| import LinearAPI from 'dashboard/api/integrations/linear'; | import LinearAPI from 'dashboard/api/integrations/linear'; | ||||||
| import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue'; | import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue'; | ||||||
| import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue'; | import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue'; | ||||||
|  | import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   conversationId: { |   conversationId: { | ||||||
| @@ -97,6 +100,7 @@ const onClose = () => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const onSearch = async value => { | const onSearch = async value => { | ||||||
|  |   issues.value = []; | ||||||
|   if (!value) return; |   if (!value) return; | ||||||
|   searchQuery.value = value; |   searchQuery.value = value; | ||||||
|   try { |   try { | ||||||
| @@ -107,7 +111,11 @@ const onSearch = async value => { | |||||||
|       name: `${issue.identifier} ${issue.title}`, |       name: `${issue.identifier} ${issue.title}`, | ||||||
|     })); |     })); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } finally { |   } finally { | ||||||
|     isFetching.value = false; |     isFetching.value = false; | ||||||
|   } |   } | ||||||
| @@ -123,7 +131,11 @@ const linkIssue = async () => { | |||||||
|     issues.value = []; |     issues.value = []; | ||||||
|     onClose(); |     onClose(); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } finally { |   } finally { | ||||||
|     isLinking.value = false; |     isLinking.value = false; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -27,7 +27,8 @@ | |||||||
|     <woot-modal |     <woot-modal | ||||||
|       :show.sync="shouldShowPopup" |       :show.sync="shouldShowPopup" | ||||||
|       :on-close="closePopup" |       :on-close="closePopup" | ||||||
|       class="!items-start [&>div]:!top-12" |       :close-on-backdrop-click="false" | ||||||
|  |       class="!items-start [&>div]:!top-12 [&>div]:sticky" | ||||||
|     > |     > | ||||||
|       <create-or-link-issue |       <create-or-link-issue | ||||||
|         :conversation="conversation" |         :conversation="conversation" | ||||||
| @@ -39,13 +40,18 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
| import { computed, ref, onMounted, watch } from 'vue'; | import { computed, ref, onMounted, watch, defineComponent, provide } from 'vue'; | ||||||
| import { useAlert } from 'dashboard/composables'; | import { useAlert } from 'dashboard/composables'; | ||||||
| import { useStoreGetters } from 'dashboard/composables/store'; | import { useStoreGetters } from 'dashboard/composables/store'; | ||||||
| import { useI18n } from 'dashboard/composables/useI18n'; | import { useI18n } from 'dashboard/composables/useI18n'; | ||||||
| import LinearAPI from 'dashboard/api/integrations/linear'; | import LinearAPI from 'dashboard/api/integrations/linear'; | ||||||
| import CreateOrLinkIssue from './CreateOrLinkIssue.vue'; | import CreateOrLinkIssue from './CreateOrLinkIssue.vue'; | ||||||
| import Issue from './Issue.vue'; | import Issue from './Issue.vue'; | ||||||
|  | import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; | ||||||
|  |  | ||||||
|  | defineComponent({ | ||||||
|  |   name: 'Linear', | ||||||
|  | }); | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   conversationId: { |   conversationId: { | ||||||
| @@ -60,6 +66,9 @@ const { t } = useI18n(); | |||||||
| const linkedIssue = ref(null); | const linkedIssue = ref(null); | ||||||
| const shouldShow = ref(false); | const shouldShow = ref(false); | ||||||
| const shouldShowPopup = ref(false); | const shouldShowPopup = ref(false); | ||||||
|  | const isUnlinking = ref(false); | ||||||
|  |  | ||||||
|  | provide('isUnlinking', isUnlinking); | ||||||
|  |  | ||||||
| const currentAccountId = getters.getCurrentAccountId; | const currentAccountId = getters.getCurrentAccountId; | ||||||
|  |  | ||||||
| @@ -80,17 +89,28 @@ const loadLinkedIssue = async () => { | |||||||
|     const issues = response.data; |     const issues = response.data; | ||||||
|     linkedIssue.value = issues && issues.length ? issues[0] : null; |     linkedIssue.value = issues && issues.length ? issues[0] : null; | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(error?.message || t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const unlinkIssue = async linkId => { | const unlinkIssue = async linkId => { | ||||||
|   try { |   try { | ||||||
|  |     isUnlinking.value = true; | ||||||
|     await LinearAPI.unlinkIssue(linkId); |     await LinearAPI.unlinkIssue(linkId); | ||||||
|     linkedIssue.value = null; |     linkedIssue.value = null; | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS')); |     useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS')); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR')); |     const errorMessage = parseLinearAPIErrorResponse( | ||||||
|  |       error, | ||||||
|  |       t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR') | ||||||
|  |     ); | ||||||
|  |     useAlert(errorMessage); | ||||||
|  |   } finally { | ||||||
|  |     isUnlinking.value = false; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -218,7 +218,7 @@ | |||||||
|         "ERROR": "There was an error fetching the linear issues, please try again", |         "ERROR": "There was an error fetching the linear issues, please try again", | ||||||
|         "LINK_SUCCESS": "Issue linked successfully", |         "LINK_SUCCESS": "Issue linked successfully", | ||||||
|         "LINK_ERROR": "There was an error linking the issue, please try again", |         "LINK_ERROR": "There was an error linking the issue, please try again", | ||||||
|         "LINK_TITLE": "#%{conversationId}: %{name}" |         "LINK_TITLE": "Conversation (#%{conversationId}) with %{name}" | ||||||
|       }, |       }, | ||||||
|       "ADD_OR_LINK": { |       "ADD_OR_LINK": { | ||||||
|         "TITLE": "Create/link linear issue", |         "TITLE": "Create/link linear issue", | ||||||
| @@ -235,22 +235,34 @@ | |||||||
|           }, |           }, | ||||||
|           "TEAM": { |           "TEAM": { | ||||||
|             "LABEL": "Team", |             "LABEL": "Team", | ||||||
|  |             "PLACEHOLDER": "Select team", | ||||||
|  |             "SEARCH": "Search team", | ||||||
|             "REQUIRED_ERROR": "Team is required" |             "REQUIRED_ERROR": "Team is required" | ||||||
|           }, |           }, | ||||||
|           "ASSIGNEE": { |           "ASSIGNEE": { | ||||||
|             "LABEL": "Assignee" |             "LABEL": "Assignee", | ||||||
|  |             "PLACEHOLDER": "Select assignee", | ||||||
|  |             "SEARCH": "Search assignee" | ||||||
|           }, |           }, | ||||||
|           "PRIORITY": { |           "PRIORITY": { | ||||||
|             "LABEL": "Priority" |             "LABEL": "Priority", | ||||||
|  |             "PLACEHOLDER": "Select priority", | ||||||
|  |             "SEARCH": "Search priority" | ||||||
|           }, |           }, | ||||||
|           "LABEL": { |           "LABEL": { | ||||||
|             "LABEL": "Label" |             "LABEL": "Label", | ||||||
|  |             "PLACEHOLDER": "Select label", | ||||||
|  |             "SEARCH": "Search label" | ||||||
|           }, |           }, | ||||||
|           "STATUS": { |           "STATUS": { | ||||||
|             "LABEL": "Status" |             "LABEL": "Status", | ||||||
|  |             "PLACEHOLDER": "Select status", | ||||||
|  |             "SEARCH": "Search status" | ||||||
|           }, |           }, | ||||||
|           "PROJECT": { |           "PROJECT": { | ||||||
|             "LABEL": "Project" |             "LABEL": "Project", | ||||||
|  |             "PLACEHOLDER": "Select project", | ||||||
|  |             "SEARCH": "Search project" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "CREATE": "Create", |         "CREATE": "Create", | ||||||
|   | |||||||
| @@ -96,3 +96,9 @@ export const throwErrorMessage = error => { | |||||||
|   const errorMessage = parseAPIErrorResponse(error); |   const errorMessage = parseAPIErrorResponse(error); | ||||||
|   throw new Error(errorMessage); |   throw new Error(errorMessage); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const parseLinearAPIErrorResponse = (error, defaultMessage) => { | ||||||
|  |   const errorData = error.response.data; | ||||||
|  |   const errorMessage = errorData?.error?.errors?.[0]?.message || defaultMessage; | ||||||
|  |   return errorMessage; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { | |||||||
|   parseAPIErrorResponse, |   parseAPIErrorResponse, | ||||||
|   setLoadingStatus, |   setLoadingStatus, | ||||||
|   throwErrorMessage, |   throwErrorMessage, | ||||||
|  |   parseLinearAPIErrorResponse, | ||||||
| } from '../api'; | } from '../api'; | ||||||
|  |  | ||||||
| describe('#getLoadingStatus', () => { | describe('#getLoadingStatus', () => { | ||||||
| @@ -49,3 +50,26 @@ describe('#throwErrorMessage', () => { | |||||||
|     expect(errorFn).toThrow('Error Message [message]'); |     expect(errorFn).toThrow('Error Message [message]'); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | describe('#parseLinearAPIErrorResponse', () => { | ||||||
|  |   it('returns correct values', () => { | ||||||
|  |     expect( | ||||||
|  |       parseLinearAPIErrorResponse( | ||||||
|  |         { | ||||||
|  |           response: { | ||||||
|  |             data: { | ||||||
|  |               error: { | ||||||
|  |                 errors: [ | ||||||
|  |                   { | ||||||
|  |                     message: 'Error Message [message]', | ||||||
|  |                   }, | ||||||
|  |                 ], | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         'Default Message' | ||||||
|  |       ) | ||||||
|  |     ).toBe('Error Message [message]'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ module Linear::Mutations | |||||||
|     case value |     case value | ||||||
|     when String |     when String | ||||||
|       # Strings must be enclosed in double quotes |       # Strings must be enclosed in double quotes | ||||||
|       "\"#{value}\"" |       "\"#{value.gsub("\n", '\\n')}\"" | ||||||
|     when Array |     when Array | ||||||
|       # Arrays need to be recursively converted |       # Arrays need to be recursively converted | ||||||
|       "[#{value.map { |v| graphql_value(v) }.join(', ')}]" |       "[#{value.map { |v| graphql_value(v) }.join(', ')}]" | ||||||
|   | |||||||
| @@ -148,6 +148,23 @@ describe Linear do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     context 'when the description is markdown' do | ||||||
|  |       let(:description) { 'Cmd/Ctrl` `K` **is our most powerful feature.** \n\nUse it to search for or take any action in the app' } | ||||||
|  |  | ||||||
|  |       before do | ||||||
|  |         stub_request(:post, url) | ||||||
|  |           .to_return(status: 200, body: { success: true, | ||||||
|  |                                           data: { issueCreate: { id: 'issue1', title: 'Title', | ||||||
|  |                                                                  description: description } } }.to_json, headers: headers) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'creates an issue' do | ||||||
|  |         response = linear_client.create_issue(params) | ||||||
|  |         expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title', | ||||||
|  |                                                     'description' => description } }) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|     context 'when the API response is an error' do |     context 'when the API response is an error' do | ||||||
|       before do |       before do | ||||||
|         stub_request(:post, url) |         stub_request(:post, url) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth