mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 12:08:01 +00:00
feat: Update the design for integration page (#9825)
Combine integrations and applications page into one page. <img width="1182" alt="Screenshot 2024-07-23 at 3 30 51 PM" src="https://github.com/user-attachments/assets/50920a6f-606f-44b3-b1e4-641046a14444"> Major changes: - The app enabled?, active? checks are all moved to backend. - The dashboard_apps integration is also now part of the apps.yml file. - Updated the header design for the new settings pages. - Merged the folders integrationapps and integrations. - Updated the copy to match the size of the card and provide clear instruction. - Only the list page is updated in this PR, rest of the pages are yet to be migrated. | Integration | Verified | | -- | -- | | Dashboard Apps | ✅ | | Dyte | ✅ | | Slack | ✅ | | Webhooks | ✅ | | Dialogflow | ✅ | | Google Translate | ✅ | | OpenAI | ✅ | | Linear | ✅ | --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'shared/assets/fonts/plus-jakarta';
|
||||
@import 'shared/assets/fonts/InterDisplay/inter-display';
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
@import 'shared/assets/stylesheets/animations';
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@@ -42,6 +45,7 @@
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
@@ -213,6 +217,7 @@
|
||||
--color-orange-800: 204 78 0;
|
||||
--color-orange-900: 88 45 29;
|
||||
}
|
||||
|
||||
// scss-lint:disable QualifyingElement
|
||||
body.dark {
|
||||
--color-amber-25: 31 19 0;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
showOnCustomBrandedInstance: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const isACustomBrandedInstance =
|
||||
getters['globalConfig/isACustomBrandedInstance'];
|
||||
|
||||
const shouldShowContent = computed(
|
||||
() => props.showOnCustomBrandedInstance || !isACustomBrandedInstance.value
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldShowContent">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -163,17 +163,6 @@ const settings = accountId => ({
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'star-emphasis',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"HEADER": "Integrations",
|
||||
"LOADING": "Fetching integrations",
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||
"FORM": {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"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",
|
||||
"LOADING": "Fetching integrations",
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||
"FORM": {
|
||||
@@ -37,7 +40,10 @@
|
||||
"LIST": {
|
||||
"404": "There are no webhooks configured for this account.",
|
||||
"TITLE": "Manage webhooks",
|
||||
"TABLE_HEADER": ["Webhook endpoint", "Actions"]
|
||||
"TABLE_HEADER": [
|
||||
"Webhook endpoint",
|
||||
"Actions"
|
||||
]
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
@@ -169,7 +175,10 @@
|
||||
"LIST": {
|
||||
"404": "There are no dashboard apps configured on this account yet",
|
||||
"LOADING": "Fetching dashboard apps...",
|
||||
"TABLE_HEADER": ["Name", "Endpoint"],
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Endpoint"
|
||||
],
|
||||
"EDIT_TOOLTIP": "Edit app",
|
||||
"DELETE_TOOLTIP": "Delete app"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SLA": {
|
||||
"HEADER": "SLA",
|
||||
"HEADER": "Service Level Agreements",
|
||||
"ADD_ACTION": "Add SLA",
|
||||
"ADD_ACTION_LONG": "Create a new SLA Policy",
|
||||
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
||||
@@ -105,4 +105,4 @@
|
||||
"HIDE": "Hide {count} rows"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full h-full px-5 pt-8 pb-3 m-0 overflow-auto bg-white sm:px-16 sm:pt-16 dark:bg-slate-900"
|
||||
class="flex flex-col w-full h-full m-0 px-8 lg:px-16 py-8 overflow-auto bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-start max-w-[900px] w-full">
|
||||
<div class="flex items-start w-full max-w-6xl mx-auto">
|
||||
<keep-alive v-if="keepAlive">
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
import CustomBrandPolicyWrapper from 'dashboard/components/CustomBrandPolicyWrapper.vue';
|
||||
import { getHelpUrlForFeature } from '../../../../helper/featureHelper';
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -9,10 +11,6 @@ defineProps({
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
@@ -20,8 +18,14 @@ defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
featureName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const helpURL = getHelpUrlForFeature(props.featureName);
|
||||
|
||||
const openInNewTab = url => {
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener noreferrer');
|
||||
@@ -29,12 +33,11 @@ const openInNewTab = url => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-4">
|
||||
<!-- Header section with icon, title and action button -->
|
||||
<div class="flex flex-col items-start w-full gap-3 pt-4">
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<!-- Icon and title container -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="iconName"
|
||||
class="flex items-center w-10 h-10 p-1 rounded-full bg-woot-25/60 dark:bg-woot-900/60"
|
||||
>
|
||||
<div
|
||||
@@ -49,7 +52,7 @@ const openInNewTab = url => {
|
||||
</div>
|
||||
</div>
|
||||
<h1
|
||||
class="text-2xl font-medium tracking-[-1.5%] text-slate-900 dark:text-slate-25"
|
||||
class="text-2xl font-semibold font-interDisplay tracking-[0.3px] text-slate-900 dark:text-slate-25"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
@@ -59,44 +62,43 @@ const openInNewTab = url => {
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Description and optional link -->
|
||||
<div
|
||||
class="flex flex-col gap-2 text-slate-600 dark:text-slate-300 max-w-[721px] w-full"
|
||||
>
|
||||
<div class="flex flex-col gap-3 text-slate-600 dark:text-slate-300 w-full">
|
||||
<p
|
||||
class="mb-0 text-sm font-normal tracking-[0.5%] line-clamp-5 sm:line-clamp-none"
|
||||
class="mb-0 text-base font-normal line-clamp-5 sm:line-clamp-none max-w-3xl"
|
||||
>
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
<!-- Conditional link -->
|
||||
<a
|
||||
v-if="href && linkText"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="sm:inline-flex hidden tracking-[-0.6%] gap-1 w-fit items-center text-woot-500 dark:text-woot-500 text-sm font-medium tracking=[-0.6%] hover:underline"
|
||||
>
|
||||
{{ linkText }}
|
||||
<fluent-icon
|
||||
size="16"
|
||||
icon="chevron-right"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
/>
|
||||
</a>
|
||||
<CustomBrandPolicyWrapper :show-on-custom-branded-instance="false">
|
||||
<a
|
||||
v-if="helpURL && linkText"
|
||||
:href="helpURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="sm:inline-flex hidden tracking-[-0.6%] gap-1 w-fit items-center text-woot-500 dark:text-woot-500 text-sm font-medium tracking=[-0.6%] hover:underline"
|
||||
>
|
||||
{{ linkText }}
|
||||
<fluent-icon
|
||||
size="16"
|
||||
icon="chevron-right"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
/>
|
||||
</a>
|
||||
</CustomBrandPolicyWrapper>
|
||||
</div>
|
||||
<!-- Mobile view for actions and link -->
|
||||
<div class="flex items-start justify-start w-full gap-3 sm:hidden">
|
||||
<slot name="actions" />
|
||||
<woot-button
|
||||
v-if="href && linkText"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-outwards"
|
||||
class="flex-row-reverse rounded-xl min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
|
||||
@click="openInNewTab(href)"
|
||||
>
|
||||
{{ linkText }}
|
||||
</woot-button>
|
||||
<CustomBrandPolicyWrapper :show-on-custom-branded-instance="false">
|
||||
<woot-button
|
||||
v-if="helpURL && linkText"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-outwards"
|
||||
class="flex-row-reverse rounded-xl min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
|
||||
@click="openInNewTab(helpURL)"
|
||||
>
|
||||
{{ linkText }}
|
||||
</woot-button>
|
||||
</CustomBrandPolicyWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,16 +12,15 @@ defineProps({
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex relative flex-col sm:flex-row p-4 gap-4 sm:p-6 justify-between shadow-sm group bg-white border border-solid rounded-xl dark:bg-slate-800 border-slate-75 dark:border-slate-700/50 max-w-[900px] w-full"
|
||||
class="flex relative flex-col sm:flex-row p-4 gap-4 sm:p-6 justify-between shadow-sm group bg-white border border-solid rounded-xl dark:bg-slate-800 border-slate-75 dark:border-slate-700/50 w-full"
|
||||
>
|
||||
<!-- left side section -->
|
||||
<slot name="leftSection">
|
||||
<div class="flex flex-col min-w-0 items-start gap-3 max-w-[480px] w-full">
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-3 sm:justify-normal whitespace-nowrap"
|
||||
>
|
||||
<h3
|
||||
class="justify-between text-sm tracking-[-0.6%] font-medium truncate w-fit sm:justify-normal text-slate-900 dark:text-slate-25"
|
||||
class="justify-between text-sm font-medium truncate w-fit sm:justify-normal text-slate-900 dark:text-slate-25"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
@@ -30,7 +29,7 @@ defineProps({
|
||||
<slot name="label" />
|
||||
</div>
|
||||
<p
|
||||
class="text-sm text-slate-600 tracking-[0.5%] dark:text-slate-300 max-w-[400px] w-full line-clamp-2"
|
||||
class="text-base text-slate-600 dark:text-slate-300 max-w-[400px] w-full line-clamp-2"
|
||||
>
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto">
|
||||
<div class="flex flex-col">
|
||||
<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 enabledIntegrations"
|
||||
:key="item.id"
|
||||
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"
|
||||
:integration-logo="item.logo"
|
||||
:integration-name="item.name"
|
||||
:integration-description="item.description"
|
||||
:integration-enabled="item.hooks.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('integrations/get');
|
||||
});
|
||||
</script>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="flex h-[6.25rem] w-[6.25rem]">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
class="max-w-full p-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center m-0 mx-4 flex-1">
|
||||
<h3 class="text-xl font-medium mb-1 text-slate-800 dark:text-slate-100">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-200">
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<woot-label
|
||||
:title="labelText"
|
||||
:color-scheme="labelColor"
|
||||
class="text-xs rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<router-link
|
||||
:to="
|
||||
frontendURL(
|
||||
`accounts/${accountId}/settings/applications/` + integrationId
|
||||
)
|
||||
"
|
||||
>
|
||||
<woot-button icon="settings">
|
||||
{{ $t('INTEGRATION_APPS.CONFIGURE') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import WootLabel from 'dashboard/components/ui/Label.vue';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootLabel,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationLogo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationDescription: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationEnabled: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
labelText() {
|
||||
return this.integrationEnabled
|
||||
? this.$t('INTEGRATION_APPS.STATUS.ENABLED')
|
||||
: this.$t('INTEGRATION_APPS.STATUS.DISABLED');
|
||||
},
|
||||
labelColor() {
|
||||
return this.integrationEnabled ? 'success' : 'secondary';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,47 +0,0 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const IntegrationHooks = () => import('./IntegrationHooks.vue');
|
||||
const Index = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/applications'),
|
||||
component: SettingsContent,
|
||||
props: params => {
|
||||
const showBackButton = params.name !== 'settings_applications';
|
||||
const backUrl =
|
||||
params.name === 'settings_applications_integration'
|
||||
? { name: 'settings_applications' }
|
||||
: '';
|
||||
return {
|
||||
headerTitle: 'INTEGRATION_APPS.HEADER',
|
||||
icon: 'star-emphasis',
|
||||
showBackButton,
|
||||
backUrl,
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_applications',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_applications_integration',
|
||||
component: IntegrationHooks,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => ({
|
||||
integrationId: route.params.integration_id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,57 +1,51 @@
|
||||
<script setup>
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import IntegrationItem from './IntegrationItem.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const uiFlags = getters['integrations/getUIFlags'];
|
||||
|
||||
const integrationList = computed(
|
||||
() => getters['integrations/getAppIntegrations'].value
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('integrations/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-shrink flex-grow overflow-auto p-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('INTEGRATION_SETTINGS.LOADING')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
:description="$t('INTEGRATION_SETTINGS.DESCRIPTION')"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.LEARN_MORE')"
|
||||
feature-name="integrations"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex-grow flex-shrink overflow-auto font-inter">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<integration-item
|
||||
v-for="item in integrationList"
|
||||
:id="item.id"
|
||||
: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"
|
||||
>
|
||||
<integration
|
||||
:integration-id="item.id"
|
||||
:integration-logo="item.logo"
|
||||
:integration-name="item.name"
|
||||
:integration-description="item.description"
|
||||
:integration-enabled="item.enabled"
|
||||
:integration-action="item.action"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 border border-solid border-slate-75 dark:border-slate-700/50 rounded-sm mb-4 p-4"
|
||||
>
|
||||
<integration
|
||||
integration-id="dashboard_apps"
|
||||
:integration-name="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.TITLE')
|
||||
"
|
||||
:integration-description="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DESCRIPTION')
|
||||
"
|
||||
integration-enabled
|
||||
integration-action="/dashboard-apps"
|
||||
/>
|
||||
</div>
|
||||
:logo="item.logo"
|
||||
:name="item.name"
|
||||
:description="item.description"
|
||||
:enabled="item.enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Integration from './Integration.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Integration,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
integrationsList: 'integrations/getIntegrations',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<!-- to be removed non using component -->
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const accountId = getters.getCurrentAccountId;
|
||||
const globalConfig = getters['globalConfig/get'];
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const integrationStatus = computed(() =>
|
||||
props.enabled
|
||||
? t('INTEGRATION_APPS.STATUS.ENABLED')
|
||||
: t('INTEGRATION_APPS.STATUS.DISABLED')
|
||||
);
|
||||
|
||||
const integrationStatusColor = computed(() =>
|
||||
props.enabled ? 'bg-green-500' : 'bg-slate-200'
|
||||
);
|
||||
|
||||
const actionURL = computed(() =>
|
||||
frontendURL(`accounts/${accountId.value}/settings/integrations/${props.id}`)
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col flex-1 p-6 bg-white border border-solid rounded-md dark:bg-slate-800 border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex h-12 w-12 mb-4">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm block dark:hidden bg-white dark:bg-slate-900"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}-dark.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm hidden dark:block bg-white dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
<fluent-icon
|
||||
v-tooltip="integrationStatus"
|
||||
size="20"
|
||||
class="text-white p-0.5 rounded-full"
|
||||
:class="integrationStatusColor"
|
||||
icon="checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col m-0 flex-1">
|
||||
<div
|
||||
class="font-medium mb-2 text-slate-800 dark:text-slate-100 flex justify-between items-center"
|
||||
>
|
||||
<span class="text-base font-semibold">{{ name }}</span>
|
||||
<router-link :to="actionURL">
|
||||
<woot-button class="clear link">
|
||||
{{ $t('INTEGRATION_APPS.CONFIGURE') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-200">
|
||||
{{ useInstallationName(description, globalConfig.installationName) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,14 +1,30 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const IntegrationHooks = () => import('./IntegrationHooks.vue');
|
||||
const Index = () => import('./Index.vue');
|
||||
const Webhook = () => import('./Webhooks/Index.vue');
|
||||
const DashboardApps = () => import('./DashboardApps/Index.vue');
|
||||
const ShowIntegration = () => import('./ShowIntegration.vue');
|
||||
const Slack = () => import('./Slack.vue');
|
||||
const Index = () => import('./Index.vue');
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/integrations'),
|
||||
component: SettingsWrapper,
|
||||
props: {},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_applications',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/integrations'),
|
||||
component: SettingsContent,
|
||||
@@ -26,14 +42,6 @@ export default {
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_integrations',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'webhook',
|
||||
component: Webhook,
|
||||
@@ -61,17 +69,14 @@ export default {
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_integrations_integration',
|
||||
component: ShowIntegration,
|
||||
name: 'settings_applications_integration',
|
||||
component: IntegrationHooks,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => {
|
||||
return {
|
||||
integrationId: route.params.integration_id,
|
||||
code: route.query.code,
|
||||
};
|
||||
},
|
||||
props: route => ({
|
||||
integrationId: route.params.integration_id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import billing from './billing/billing.routes';
|
||||
import campaigns from './campaigns/campaigns.routes';
|
||||
import canned from './canned/canned.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
import integrationapps from './integrationapps/integrations.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import labels from './labels/labels.routes';
|
||||
import macros from './macros/macros.routes';
|
||||
@@ -44,7 +43,6 @@ export default {
|
||||
...campaigns.routes,
|
||||
...canned.routes,
|
||||
...inbox.routes,
|
||||
...integrationapps.routes,
|
||||
...integrations.routes,
|
||||
...labels.routes,
|
||||
...macros.routes,
|
||||
|
||||
@@ -7,14 +7,15 @@ defineProps({
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
<template>
|
||||
<base-settings-header
|
||||
<BaseSettingsHeader
|
||||
:title="$t('SLA.HEADER')"
|
||||
:description="$t('SLA.DESCRIPTION')"
|
||||
:link-text="$t('SLA.LEARN_MORE')"
|
||||
href="/"
|
||||
icon-name="document-list-clock"
|
||||
feature-name="sla"
|
||||
>
|
||||
<template v-if="showActions" #actions>
|
||||
<woot-button
|
||||
@@ -26,5 +27,5 @@ defineProps({
|
||||
{{ $t('SLA.ADD_ACTION') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</base-settings-header>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
@@ -20,21 +20,9 @@ const state = {
|
||||
},
|
||||
};
|
||||
|
||||
const isAValidAppIntegration = integration => {
|
||||
return [
|
||||
'dialogflow',
|
||||
'dyte',
|
||||
'google_translate',
|
||||
'openai',
|
||||
'linear',
|
||||
].includes(integration.id);
|
||||
};
|
||||
export const getters = {
|
||||
getIntegrations($state) {
|
||||
return $state.records.filter(item => !isAValidAppIntegration(item));
|
||||
},
|
||||
getAppIntegrations($state) {
|
||||
return $state.records.filter(item => isAValidAppIntegration(item));
|
||||
return $state.records;
|
||||
},
|
||||
getIntegration: $state => integrationId => {
|
||||
const [integration] = $state.records.filter(
|
||||
|
||||
@@ -1,60 +1,9 @@
|
||||
import { getters } from '../../integrations';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getIntegrations', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 'test1',
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'test2',
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dyte',
|
||||
name: 'dyte',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dialogflow',
|
||||
name: 'dialogflow',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getIntegrations(state)).toEqual([
|
||||
{
|
||||
id: 'test1',
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'test2',
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getAppIntegrations', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 'test1',
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dyte',
|
||||
name: 'dyte',
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
export const useInstallationName = (str, installationName) => {
|
||||
if (str && installationName) {
|
||||
return str.replace(/Chatwoot/g, installationName);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
// eslint-disable-next-line default-param-last
|
||||
useInstallationName(str = '', installationName) {
|
||||
return str.replace(/Chatwoot/g, installationName);
|
||||
},
|
||||
useInstallationName,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user