chore: Linear integration fixes (#9538)
This commit is contained in:
parent
59b912f22c
commit
a55fffab3a
@ -99,7 +99,9 @@ export default {
|
|||||||
onMouseUp() {
|
onMouseUp() {
|
||||||
if (this.mousedDownOnBackdrop) {
|
if (this.mousedDownOnBackdrop) {
|
||||||
this.mousedDownOnBackdrop = false;
|
this.mousedDownOnBackdrop = false;
|
||||||
this.onClose();
|
if (this.closeOnBackdropClick) {
|
||||||
|
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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user