feat(apps): Shopify Integration (#11101)
This PR adds native integration with Shopify. No more dashboard apps. The support agents can view the orders, their status and the link to the order page on the conversation sidebar. This PR does the following: - Create an integration with Shopify (a new app is added in the integrations tab) - Option to configure it in SuperAdmin - OAuth endpoint and the callbacks. - Frontend component to render the orders. (We might need to cache it in the future) --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -32,6 +32,12 @@ class IntegrationsAPI extends ApiClient {
|
||||
deleteHook(hookId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
|
||||
connectShopify({ shopDomain }) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, {
|
||||
shop_domain: shopDomain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new IntegrationsAPI();
|
||||
|
||||
17
app/javascript/dashboard/api/integrations/shopify.js
Normal file
17
app/javascript/dashboard/api/integrations/shopify.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class ShopifyAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('integrations/shopify', { accountScoped: true });
|
||||
}
|
||||
|
||||
getOrders(contactId) {
|
||||
return axios.get(`${this.url}/orders`, {
|
||||
params: { contact_id: contactId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ShopifyAPI();
|
||||
@@ -80,10 +80,12 @@ const maxWidthClass = computed(() => {
|
||||
const open = () => {
|
||||
dialogRef.value?.showModal();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
emit('confirm');
|
||||
};
|
||||
@@ -104,9 +106,10 @@ defineExpose({ open, close });
|
||||
@close="close"
|
||||
>
|
||||
<OnClickOutside @trigger="close">
|
||||
<div
|
||||
<form
|
||||
ref="dialogContentRef"
|
||||
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
|
||||
@submit.prevent="confirm"
|
||||
@click.stop
|
||||
>
|
||||
<div v-if="title || description" class="flex flex-col gap-2">
|
||||
@@ -129,6 +132,7 @@ defineExpose({ open, close });
|
||||
color="slate"
|
||||
:label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
|
||||
class="w-full"
|
||||
type="button"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
@@ -138,11 +142,11 @@ defineExpose({ open, close });
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
:disabled="disableConfirmButton || isLoading"
|
||||
@click="confirm"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</form>
|
||||
</OnClickOutside>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
order: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formatDate = dateString => {
|
||||
return format(new Date(dateString), 'MMM d, yyyy');
|
||||
};
|
||||
|
||||
const formatCurrency = (amount, currency) => {
|
||||
return new Intl.NumberFormat('en', {
|
||||
style: 'currency',
|
||||
currency: currency || 'USD',
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getStatusClass = status => {
|
||||
const classes = {
|
||||
paid: 'bg-n-teal-5 text-n-teal-12',
|
||||
};
|
||||
return classes[status] || 'bg-slate-50 text-slate-700';
|
||||
};
|
||||
|
||||
const getStatusI18nKey = (type, status = '') => {
|
||||
return `CONVERSATION_SIDEBAR.SHOPIFY.${type.toUpperCase()}_STATUS.${status.toUpperCase()}`;
|
||||
};
|
||||
|
||||
const fulfillmentStatus = computed(() => {
|
||||
const { fulfillment_status: status } = props.order;
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
return t(getStatusI18nKey('FULFILLMENT', status));
|
||||
});
|
||||
|
||||
const financialStatus = computed(() => {
|
||||
const { financial_status: status } = props.order;
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
return t(getStatusI18nKey('FINANCIAL', status));
|
||||
});
|
||||
|
||||
const getFulfillmentClass = status => {
|
||||
const classes = {
|
||||
fulfilled: 'text-green-600',
|
||||
partial: 'text-yellow-600',
|
||||
unfulfilled: 'text-red-600',
|
||||
};
|
||||
return classes[status] || 'text-slate-600';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="py-3 border-b border-n-weak last:border-b-0 flex flex-col gap-1.5"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium flex">
|
||||
<a
|
||||
:href="order.admin_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:underline text-n-slate-12 cursor-pointer truncate"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.ORDER_ID', { id: order.id }) }}
|
||||
<i class="i-lucide-external-link pl-5" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
:class="getStatusClass(order.financial_status)"
|
||||
class="text-xs px-2 py-1 rounded capitalize truncate"
|
||||
:title="financialStatus"
|
||||
>
|
||||
{{ financialStatus }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-n-slate-12">
|
||||
<span class="text-n-slate-11 border-r border-n-weak pr-2">
|
||||
{{ formatDate(order.created_at) }}
|
||||
</span>
|
||||
<span class="text-n-slate-11 pl-2">
|
||||
{{ formatCurrency(order.total_price, order.currency) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="fulfillmentStatus">
|
||||
<span
|
||||
:class="getFulfillmentClass(order.fulfillment_status)"
|
||||
class="capitalize font-medium"
|
||||
:title="fulfillmentStatus"
|
||||
>
|
||||
{{ fulfillmentStatus }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ShopifyAPI from '../../../api/integrations/shopify';
|
||||
import ShopifyOrderItem from './ShopifyOrderItem.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contactId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const contact = useFunctionGetter('contacts/getContact', props.contactId);
|
||||
|
||||
const hasSearchableInfo = computed(
|
||||
() => !!contact.value?.email || !!contact.value?.phone_number
|
||||
);
|
||||
|
||||
const orders = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await ShopifyAPI.getOrders(props.contactId);
|
||||
orders.value = response.data.orders;
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e.response?.data?.error || 'CONVERSATION_SIDEBAR.SHOPIFY.ERROR';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contactId,
|
||||
() => {
|
||||
if (hasSearchableInfo.value) {
|
||||
fetchOrders();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-4 py-2 text-n-slate-12">
|
||||
<div v-if="!hasSearchableInfo" class="text-center text-n-slate-12">
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
|
||||
</div>
|
||||
<div v-else-if="loading" class="flex justify-center items-center p-4">
|
||||
<Spinner size="32" class="text-n-brand" />
|
||||
</div>
|
||||
<div v-else-if="error" class="text-center text-n-ruby-12">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else-if="!orders.length" class="text-center text-n-slate-12">
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShopifyOrderItem
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
:order="order"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
||||
{ name: 'contact_attributes' },
|
||||
{ name: 'previous_conversation' },
|
||||
{ name: 'conversation_participants' },
|
||||
{ name: 'shopify_orders' },
|
||||
]);
|
||||
|
||||
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
||||
|
||||
@@ -295,7 +295,27 @@
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros"
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pending",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancel",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrations",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -50,7 +50,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="input-container rounded-xl transition-[border-bottom] duration-[0.2s] ease-[ease-in-out] relative flex items-center py-2 px-4 h-14 gap-2 border border-solid"
|
||||
class="input-container rounded-xl transition-[border-bottom] duration-[0.2s] ease-[ease-in-out] relative flex items-center py-2 px-4 h-14 gap-2 border border-solid bg-n-alpha-black2"
|
||||
:class="{
|
||||
'border-n-brand': isInputFocused,
|
||||
'border-n-weak': !isInputFocused,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import {
|
||||
useMapGetter,
|
||||
useFunctionGetter,
|
||||
useStore,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
@@ -13,6 +17,7 @@ import ConversationInfo from './ConversationInfo.vue';
|
||||
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import MacrosList from './Macros/List.vue';
|
||||
import ShopifyOrdersList from '../../../components/widgets/conversation/ShopifyOrdersList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -38,6 +43,14 @@ const {
|
||||
|
||||
const dragging = ref(false);
|
||||
const conversationSidebarItems = ref([]);
|
||||
const shopifyIntegration = useFunctionGetter(
|
||||
'integrations/getIntegration',
|
||||
'shopify'
|
||||
);
|
||||
|
||||
const isShopifyFeatureEnabled = computed(
|
||||
() => shopifyIntegration.value.enabled
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
@@ -216,6 +229,22 @@ onMounted(() => {
|
||||
<MacrosList :conversation-id="conversationId" />
|
||||
</AccordionItem>
|
||||
</woot-feature-toggle>
|
||||
<div
|
||||
v-else-if="
|
||||
element.name === 'shopify_orders' && isShopifyFeatureEnabled
|
||||
"
|
||||
>
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.SHOPIFY_ORDERS')"
|
||||
:is-open="isContactSidebarItemOpen('is_shopify_orders_open')"
|
||||
compact
|
||||
@toggle="
|
||||
value => toggleSidebarUIState('is_shopify_orders_open', value)
|
||||
"
|
||||
>
|
||||
<ShopifyOrdersList :contact-id="contactId" />
|
||||
</AccordionItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useI18n } from 'vue-i18n';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -25,17 +27,21 @@ const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||
const globalConfig = computed(() => store.getters['globalConfig/get']);
|
||||
|
||||
const openDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = true;
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
const closeDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = false;
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.close();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteIntegration = async () => {
|
||||
@@ -50,16 +56,18 @@ const deleteIntegration = async () => {
|
||||
const confirmDeletion = () => {
|
||||
closeDeletePopup();
|
||||
deleteIntegration();
|
||||
router.push({ name: 'settings_integrations' });
|
||||
router.push({ name: 'settings_applications' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start justify-between md:flex-row md:items-center p-4 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow"
|
||||
class="flex flex-col items-start justify-between lg:flex-row lg:items-center p-6 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow gap-6"
|
||||
>
|
||||
<div class="flex items-center justify-start flex-1 m-0 mx-4 gap-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center">
|
||||
<div
|
||||
class="flex items-start lg:items-center justify-start flex-1 m-0 gap-6 flex-col lg:flex-row"
|
||||
>
|
||||
<div class="flex h-16 w-16 items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
@@ -83,7 +91,7 @@ const confirmDeletion = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<div class="flex justify-center items-center mb-0">
|
||||
<router-link
|
||||
:to="
|
||||
frontendURL(
|
||||
@@ -105,33 +113,37 @@ const confirmDeletion = () => {
|
||||
</div>
|
||||
<div v-else>
|
||||
<NextButton faded blue>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.CONFIGURE') }}
|
||||
{{ t('INTEGRATION_SETTINGS.WEBHOOK.CONFIGURE') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-if="!integrationEnabled">
|
||||
<a :href="integrationAction">
|
||||
<NextButton faded blue>
|
||||
{{ $t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT') }}
|
||||
</NextButton>
|
||||
</a>
|
||||
<slot name="action">
|
||||
<a :href="integrationAction">
|
||||
<NextButton faded blue>
|
||||
{{ t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT') }}
|
||||
</NextButton>
|
||||
</a>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
v-model:show="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="
|
||||
deleteConfirmationText.title ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')
|
||||
t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')
|
||||
"
|
||||
:message="
|
||||
:description="
|
||||
deleteConfirmationText.message ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')
|
||||
t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')
|
||||
"
|
||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
:confirm-button-label="
|
||||
t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')
|
||||
"
|
||||
:cancel-button-label="t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
@confirm="confirmDeletion"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import {
|
||||
useFunctionGetter,
|
||||
useMapGetter,
|
||||
useStore,
|
||||
} from 'dashboard/composables/store';
|
||||
import Integration from './Integration.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import integrationAPI from 'dashboard/api/integrations';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const dialogRef = ref(null);
|
||||
const integrationLoaded = ref(false);
|
||||
const storeUrl = ref('');
|
||||
const isSubmitting = ref(false);
|
||||
const storeUrlError = ref('');
|
||||
const integration = useFunctionGetter('integrations/getIntegration', 'shopify');
|
||||
const uiFlags = useMapGetter('integrations/getUIFlags');
|
||||
|
||||
const integrationAction = computed(() => {
|
||||
if (integration.value.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return 'connect';
|
||||
});
|
||||
|
||||
const hideStoreUrlModal = () => {
|
||||
storeUrl.value = '';
|
||||
storeUrlError.value = '';
|
||||
isSubmitting.value = false;
|
||||
};
|
||||
|
||||
const validateStoreUrl = url => {
|
||||
const pattern = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/;
|
||||
return pattern.test(url);
|
||||
};
|
||||
|
||||
const openStoreUrlDialog = () => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStoreUrlSubmit = async () => {
|
||||
try {
|
||||
storeUrlError.value = '';
|
||||
if (!validateStoreUrl(storeUrl.value)) {
|
||||
storeUrlError.value =
|
||||
'Please enter a valid Shopify store URL (e.g., your-store.myshopify.com)';
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
const { data } = await integrationAPI.connectShopify({
|
||||
shopDomain: storeUrl.value,
|
||||
});
|
||||
|
||||
if (data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
} catch (error) {
|
||||
storeUrlError.value = error.message;
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeShopifyIntegration = async () => {
|
||||
await store.dispatch('integrations/get', 'shopify');
|
||||
integrationLoaded.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeShopifyIntegration();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto max-w-6xl mx-auto">
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingShopify"
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.MESSAGE'),
|
||||
}"
|
||||
>
|
||||
<template #action>
|
||||
<button
|
||||
class="rounded button success nice"
|
||||
@click="openStoreUrlDialog"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT') }}
|
||||
</button>
|
||||
</template>
|
||||
</Integration>
|
||||
<div
|
||||
v-if="error"
|
||||
class="flex items-center justify-center flex-1 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow p-6"
|
||||
>
|
||||
<p class="text-red-500">
|
||||
{{ $t('INTEGRATION_SETTINGS.SHOPIFY.ERROR') }}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.TITLE')"
|
||||
:is-loading="isSubmitting"
|
||||
@confirm="handleStoreUrlSubmit"
|
||||
@close="hideStoreUrlModal"
|
||||
>
|
||||
<Input
|
||||
v-model="storeUrl"
|
||||
:label="$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.LABEL')"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.PLACEHOLDER')
|
||||
"
|
||||
:message="
|
||||
!storeUrlError
|
||||
? $t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.HELP')
|
||||
: storeUrlError
|
||||
"
|
||||
:message-type="storeUrlError ? 'error' : 'info'"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,6 +8,8 @@ import DashboardApps from './DashboardApps/Index.vue';
|
||||
import Slack from './Slack.vue';
|
||||
import SettingsContent from '../Wrapper.vue';
|
||||
import Linear from './Linear.vue';
|
||||
import Shopify from './Shopify.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
@@ -88,6 +90,16 @@ export default {
|
||||
},
|
||||
props: route => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
path: 'shopify',
|
||||
name: 'settings_integrations_shopify',
|
||||
component: Shopify,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => ({ error: route.query.error }),
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_applications_integration',
|
||||
|
||||
Reference in New Issue
Block a user