feat(channels): add Shangwutong connector
This commit is contained in:
@@ -4,6 +4,7 @@ import Facebook from './channels/Facebook.vue';
|
||||
import Website from './channels/Website.vue';
|
||||
import Twitter from './channels/Twitter.vue';
|
||||
import Api from './channels/Api.vue';
|
||||
import Shangwutong from './channels/Shangwutong.vue';
|
||||
import Email from './channels/Email.vue';
|
||||
import Sms from './channels/Sms.vue';
|
||||
import Whatsapp from './channels/Whatsapp.vue';
|
||||
@@ -18,6 +19,7 @@ const channelViewList = {
|
||||
website: Website,
|
||||
twitter: Twitter,
|
||||
api: Api,
|
||||
shangwutong: Shangwutong,
|
||||
email: Email,
|
||||
sms: Sms,
|
||||
whatsapp: Whatsapp,
|
||||
|
||||
@@ -55,6 +55,14 @@ const channelList = computed(() => {
|
||||
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.API.DESCRIPTION'),
|
||||
icon: 'i-woot-api',
|
||||
},
|
||||
{
|
||||
key: 'shangwutong',
|
||||
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.SHANGWUTONG.TITLE'),
|
||||
description: t(
|
||||
'INBOX_MGMT.ADD.AUTH.CHANNEL.SHANGWUTONG.DESCRIPTION'
|
||||
),
|
||||
icon: 'i-ri-customer-service-2-line',
|
||||
},
|
||||
{
|
||||
key: 'telegram',
|
||||
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.TELEGRAM.TITLE'),
|
||||
|
||||
@@ -199,6 +199,7 @@ export default {
|
||||
this.isATwilioChannel ||
|
||||
this.isALineChannel ||
|
||||
this.isAPIInbox ||
|
||||
this.isShangwutongInbox ||
|
||||
(this.isAnEmailChannel && !this.inbox.provider) ||
|
||||
this.shouldShowWhatsAppConfiguration ||
|
||||
this.isAWebWidgetInbox
|
||||
@@ -292,6 +293,7 @@ export default {
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isAPIInbox ||
|
||||
this.isShangwutongInbox ||
|
||||
this.isAnInstagramChannel ||
|
||||
this.isALineChannel ||
|
||||
this.isATiktokChannel ||
|
||||
@@ -591,6 +593,9 @@ export default {
|
||||
this.isInboundEmailEnabled && this.continuityViaEmail,
|
||||
},
|
||||
};
|
||||
if (this.isShangwutongInbox) {
|
||||
payload.channel = {};
|
||||
}
|
||||
if (this.avatarFile) {
|
||||
payload.avatar = this.avatarFile;
|
||||
}
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import router from '../../../../index';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const validSessionId = value => /^[A-Za-z0-9]{11}$/.test(value || '');
|
||||
const validWebhookUrl = value => {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return (
|
||||
['http:', 'https:'].includes(parsed.protocol) &&
|
||||
!parsed.username &&
|
||||
!parsed.password &&
|
||||
!parsed.hash
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { PageHeader, NextButton },
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
channelName: '',
|
||||
sessionId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
desiredPresence: 'online',
|
||||
webhookUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
sessionId: { required, validSessionId },
|
||||
username: { required },
|
||||
password: { required },
|
||||
webhookUrl: { required, validWebhookUrl },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) return;
|
||||
|
||||
try {
|
||||
const inbox = await this.$store.dispatch(
|
||||
'inboxes/createChannel',
|
||||
{
|
||||
name: this.channelName.trim(),
|
||||
channel: {
|
||||
type: 'shangwutong',
|
||||
session_id: this.sessionId.trim(),
|
||||
username: this.username.trim(),
|
||||
password: this.password,
|
||||
desired_presence: this.desiredPresence,
|
||||
webhook_url: this.webhookUrl.trim(),
|
||||
},
|
||||
}
|
||||
);
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: { page: 'new', inbox_id: inbox.id },
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t(
|
||||
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full p-6 col-span-6">
|
||||
<PageHeader
|
||||
:header-title="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESC')"
|
||||
/>
|
||||
<form class="flex flex-col" @submit.prevent="createChannel">
|
||||
<label :class="{ error: v$.channelName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.CHANNEL_NAME') }}
|
||||
<input
|
||||
v-model="channelName"
|
||||
type="text"
|
||||
@blur="v$.channelName.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.sessionId.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID') }}
|
||||
<input
|
||||
v-model="sessionId"
|
||||
type="text"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
@blur="v$.sessionId.$touch"
|
||||
/>
|
||||
<span v-if="v$.sessionId.$error" class="message">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID_ERROR'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.username.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.USERNAME') }}
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
autocomplete="username"
|
||||
@blur="v$.username.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.password.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PASSWORD') }}
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
maxlength="4096"
|
||||
autocomplete="new-password"
|
||||
@blur="v$.password.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESIRED_PRESENCE') }}
|
||||
<select v-model="desiredPresence">
|
||||
<option value="online">online</option>
|
||||
<option value="busy">busy</option>
|
||||
<option value="away">away</option>
|
||||
<option value="offline">offline</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.webhookUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.WEBHOOK_URL') }}
|
||||
<input
|
||||
v-model="webhookUrl"
|
||||
type="url"
|
||||
placeholder="http://shangwutong-connector:9100/webhooks/gochat/v1"
|
||||
@blur="v$.webhookUrl.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="w-full mt-4">
|
||||
<NextButton
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:is-loading="uiFlags.isCreating"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SUBMIT_BUTTON')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import InboxHealthAPI from 'dashboard/api/inboxHealth';
|
||||
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: { SettingsFieldSection, NextButton },
|
||||
props: {
|
||||
inbox: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
desiredPresence: 'online',
|
||||
password: '',
|
||||
webhookUrl: '',
|
||||
health: null,
|
||||
isSaving: false,
|
||||
isLoadingHealth: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
inbox: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
this.desiredPresence = value.desired_presence || 'online';
|
||||
this.webhookUrl = value.webhook_url || '';
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchHealth();
|
||||
},
|
||||
methods: {
|
||||
async fetchHealth() {
|
||||
this.isLoadingHealth = true;
|
||||
try {
|
||||
const response = await InboxHealthAPI.getHealthStatus(
|
||||
this.inbox.id
|
||||
);
|
||||
this.health = response.data;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
|
||||
);
|
||||
} finally {
|
||||
this.isLoadingHealth = false;
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
this.isSaving = true;
|
||||
const channel = {
|
||||
desired_presence: this.desiredPresence,
|
||||
webhook_url: this.webhookUrl.trim(),
|
||||
};
|
||||
if (this.password) channel.password = this.password;
|
||||
try {
|
||||
await this.$store.dispatch('inboxes/updateInbox', {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel,
|
||||
});
|
||||
this.password = '';
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
await this.fetchHealth();
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
|
||||
);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS')"
|
||||
:help-text="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS_HELP')
|
||||
"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
connection_status
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.connection_status ||
|
||||
inbox.connection_status ||
|
||||
'pending'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
actual_presence
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.actual_presence ||
|
||||
inbox.actual_presence ||
|
||||
'offline'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
credential_status
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.credential_status ||
|
||||
inbox.credential_status ||
|
||||
'pending'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-n-weak p-3">
|
||||
<p class="text-label-small text-n-slate-10">
|
||||
last_heartbeat_at
|
||||
</p>
|
||||
<p class="text-body-main text-n-slate-12">
|
||||
{{
|
||||
health?.last_heartbeat_at ||
|
||||
inbox.last_heartbeat_at ||
|
||||
'—'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="health?.last_error_code"
|
||||
class="mt-3 text-sm text-n-ruby-11"
|
||||
>
|
||||
{{ health.last_error_code }}
|
||||
</p>
|
||||
<NextButton
|
||||
class="mt-3"
|
||||
:is-loading="isLoadingHealth"
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.REFRESH')"
|
||||
@click="fetchHealth"
|
||||
/>
|
||||
</SettingsFieldSection>
|
||||
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE')"
|
||||
:help-text="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE_HELP')
|
||||
"
|
||||
>
|
||||
<select v-model="desiredPresence" class="w-full">
|
||||
<option value="online">online</option>
|
||||
<option value="busy">busy</option>
|
||||
<option value="away">away</option>
|
||||
<option value="offline">offline</option>
|
||||
</select>
|
||||
</SettingsFieldSection>
|
||||
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD')"
|
||||
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD_HELP')"
|
||||
>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
maxlength="4096"
|
||||
autocomplete="new-password"
|
||||
class="w-full"
|
||||
/>
|
||||
</SettingsFieldSection>
|
||||
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL')"
|
||||
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL_HELP')"
|
||||
>
|
||||
<input v-model="webhookUrl" type="url" class="w-full" />
|
||||
</SettingsFieldSection>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<NextButton
|
||||
:is-loading="isSaving"
|
||||
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+1
@@ -32,6 +32,7 @@ const i18nMap = {
|
||||
'Channel::Telegram': 'TELEGRAM',
|
||||
'Channel::Line': 'LINE',
|
||||
'Channel::Api': 'API',
|
||||
'Channel::Shangwutong': 'SHANGWUTONG',
|
||||
'Channel::Instagram': 'INSTAGRAM',
|
||||
'Channel::Tiktok': 'TIKTOK',
|
||||
};
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ import { required } from '@vuelidate/validators';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'next/textarea/TextArea.vue';
|
||||
import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
|
||||
import ShangwutongConfiguration from '../channels/ShangwutongConfiguration.vue';
|
||||
import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -23,6 +24,7 @@ export default {
|
||||
NextButton,
|
||||
TextArea,
|
||||
WhatsappReauthorize,
|
||||
ShangwutongConfiguration,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -189,6 +191,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ShangwutongConfiguration v-if="isShangwutongInbox" :inbox="inbox" />
|
||||
<div v-if="isATwilioChannel">
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||
|
||||
Reference in New Issue
Block a user