Merge branch 'develop' into fix/next-message-bubbles
This commit is contained in:
+4
-4
@@ -498,14 +498,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.18.2)
|
||||
nokogiri (1.18.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-arm64-darwin)
|
||||
nokogiri (1.18.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-x86_64-darwin)
|
||||
nokogiri (1.18.3-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.2-x86_64-linux-gnu)
|
||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
|
||||
@@ -94,7 +94,8 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
end
|
||||
|
||||
def since_timestamp(date)
|
||||
(date - 6.days).to_i.to_s
|
||||
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
||||
(date - number_of_days).to_i.to_s
|
||||
end
|
||||
|
||||
def until_timestamp(date)
|
||||
|
||||
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
getConversationTrafficCSV() {
|
||||
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
||||
return axios.get(`${this.url}/conversation_traffic`, {
|
||||
params: { timezone_offset: getTimeOffset() },
|
||||
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<script setup>
|
||||
import { useAttrs } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const attrs = useAttrs();
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-if="globalConfig.logoThumbnail"
|
||||
v-bind="attrs"
|
||||
:src="globalConfig.logoThumbnail"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
v-once
|
||||
v-bind="attrs"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
|
||||
@@ -2,34 +2,30 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import BaseAttachmentBubble from './BaseAttachment.vue';
|
||||
|
||||
const { content, sender, contentAttributes, id } = useMessageContext();
|
||||
const { content, sender, id } = useMessageContext();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const meetingData = computed(() => {
|
||||
return useCamelCase(contentAttributes.value.data);
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
const dyteAuthToken = ref('');
|
||||
|
||||
const meetingLink = computed(() => {
|
||||
return buildDyteURL(meetingData.value.roomName, dyteAuthToken.value);
|
||||
return buildDyteURL(dyteAuthToken.value);
|
||||
});
|
||||
|
||||
const joinTheCall = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data: { authResponse: { authToken } = {} } = {} } =
|
||||
await DyteAPI.addParticipantToMeeting(id.value);
|
||||
dyteAuthToken.value = authToken;
|
||||
const { data: { token } = {} } = await DyteAPI.addParticipantToMeeting(
|
||||
id.value
|
||||
);
|
||||
dyteAuthToken.value = token;
|
||||
} catch (err) {
|
||||
useAlert(t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||
} finally {
|
||||
|
||||
@@ -525,7 +525,7 @@ const menuItems = computed(() => {
|
||||
<section class="grid gap-2 mt-2 mb-4">
|
||||
<div class="flex items-center min-w-0 gap-2 px-2">
|
||||
<div class="grid flex-shrink-0 size-6 place-content-center">
|
||||
<Logo />
|
||||
<Logo class="size-4" />
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-px h-3 bg-n-strong" />
|
||||
<SidebarAccountSwitcher
|
||||
|
||||
+5
-8
@@ -9,26 +9,23 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
meetingData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
|
||||
},
|
||||
computed: {
|
||||
meetingLink() {
|
||||
return buildDyteURL(this.meetingData.room_name, this.dyteAuthToken);
|
||||
return buildDyteURL(this.dyteAuthToken);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async joinTheCall() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const { data: { authResponse: { authToken } = {} } = {} } =
|
||||
await DyteAPI.addParticipantToMeeting(this.messageId);
|
||||
this.dyteAuthToken = authToken;
|
||||
const { data: { token } = {} } = await DyteAPI.addParticipantToMeeting(
|
||||
this.messageId
|
||||
);
|
||||
this.dyteAuthToken = token;
|
||||
} catch (err) {
|
||||
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ref, onBeforeUnmount } from 'vue';
|
||||
|
||||
export const useLiveRefresh = (callback, interval = 60000) => {
|
||||
const timeoutId = ref(null);
|
||||
|
||||
const startRefetching = () => {
|
||||
timeoutId.value = setTimeout(async () => {
|
||||
await callback();
|
||||
startRefetching();
|
||||
}, interval);
|
||||
};
|
||||
|
||||
const stopRefetching = () => {
|
||||
if (timeoutId.value) {
|
||||
clearTimeout(timeoutId.value);
|
||||
timeoutId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopRefetching();
|
||||
});
|
||||
|
||||
return {
|
||||
startRefetching,
|
||||
stopRefetching,
|
||||
};
|
||||
};
|
||||
@@ -3,13 +3,11 @@ import { mapGetters } from 'vuex';
|
||||
import AgentTable from './components/overview/AgentTable.vue';
|
||||
import MetricCard from './components/overview/MetricCard.vue';
|
||||
import { OVERVIEW_METRICS } from './constants';
|
||||
import ReportHeatmap from './components/Heatmap.vue';
|
||||
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import HeatmapContainer from './components/HeatmapContainer.vue';
|
||||
export const FETCH_INTERVAL = 60000;
|
||||
|
||||
export default {
|
||||
@@ -18,7 +16,7 @@ export default {
|
||||
ReportHeader,
|
||||
AgentTable,
|
||||
MetricCard,
|
||||
ReportHeatmap,
|
||||
HeatmapContainer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -33,7 +31,6 @@ export default {
|
||||
agents: 'agents/getAgents',
|
||||
accountConversationMetric: 'getAccountConversationMetric',
|
||||
agentConversationMetric: 'getAgentConversationMetric',
|
||||
accountConversationHeatmap: 'getAccountConversationHeatmapData',
|
||||
uiFlags: 'getOverviewUIFlags',
|
||||
}),
|
||||
agentStatusMetrics() {
|
||||
@@ -80,7 +77,6 @@ export default {
|
||||
fetchAllData() {
|
||||
this.fetchAccountConversationMetric();
|
||||
this.fetchAgentConversationMetric();
|
||||
this.fetchHeatmapData();
|
||||
},
|
||||
downloadHeatmapData() {
|
||||
let to = endOfDay(new Date());
|
||||
@@ -89,33 +85,7 @@ export default {
|
||||
to: getUnixTime(to),
|
||||
});
|
||||
},
|
||||
fetchHeatmapData() {
|
||||
if (this.uiFlags.isFetchingAccountConversationsHeatmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the data for the last 6 days won't ever change,
|
||||
// so there's no need to fetch it again
|
||||
// but we can write some logic to check if the data is already there
|
||||
// if it is there, we can refetch data only for today all over again
|
||||
// and reconcile it with the rest of the data
|
||||
// this will reduce the load on the server doing number crunching
|
||||
let to = endOfDay(new Date());
|
||||
let from = startOfDay(subDays(to, 6));
|
||||
|
||||
if (this.accountConversationHeatmap.length) {
|
||||
to = endOfDay(new Date());
|
||||
from = startOfDay(to);
|
||||
}
|
||||
|
||||
this.$store.dispatch('fetchAccountConversationHeatmap', {
|
||||
metric: 'conversations_count',
|
||||
from: getUnixTime(from),
|
||||
to: getUnixTime(to),
|
||||
groupBy: 'hour',
|
||||
businessHours: false,
|
||||
});
|
||||
},
|
||||
fetchAccountConversationMetric() {
|
||||
this.$store.dispatch('fetchAccountConversationMetric', {
|
||||
type: 'account',
|
||||
@@ -180,25 +150,7 @@ export default {
|
||||
</MetricCard>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap max-w-full">
|
||||
<MetricCard :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
|
||||
<template #control>
|
||||
<woot-button
|
||||
icon="arrow-download"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
@click="downloadHeatmapData"
|
||||
>
|
||||
{{ $t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
<ReportHeatmap
|
||||
:heat-data="accountConversationHeatmap"
|
||||
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
|
||||
/>
|
||||
</MetricCard>
|
||||
</div>
|
||||
<HeatmapContainer />
|
||||
<div class="flex flex-row flex-wrap max-w-full">
|
||||
<MetricCard :header="$t('OVERVIEW_REPORTS.AGENT_CONVERSATIONS.HEADER')">
|
||||
<AgentTable
|
||||
|
||||
@@ -10,10 +10,14 @@ import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
heatData: {
|
||||
heatmapData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
numberOfRows: {
|
||||
type: Number,
|
||||
default: 7,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -21,11 +25,11 @@ const props = defineProps({
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const processedData = computed(() => {
|
||||
return groupHeatmapByDay(props.heatData);
|
||||
return groupHeatmapByDay(props.heatmapData);
|
||||
});
|
||||
|
||||
const quantileRange = computed(() => {
|
||||
const flattendedData = props.heatData.map(data => data.value);
|
||||
const flattendedData = props.heatmapData.map(data => data.value);
|
||||
return getQuantileIntervals(flattendedData, [0.2, 0.4, 0.6, 0.8, 0.9, 0.99]);
|
||||
});
|
||||
|
||||
@@ -95,14 +99,14 @@ function getHeatmapLevelClass(value) {
|
||||
<template v-if="isLoading">
|
||||
<div class="grid gap-[5px] flex-shrink-0">
|
||||
<div
|
||||
v-for="ii in 7"
|
||||
v-for="ii in numberOfRows"
|
||||
:key="ii"
|
||||
class="w-full rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse h-8 min-w-[70px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-[5px] w-full min-w-[700px]">
|
||||
<div
|
||||
v-for="ii in 7"
|
||||
v-for="ii in numberOfRows"
|
||||
:key="ii"
|
||||
class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
|
||||
>
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import MetricCard from './overview/MetricCard.vue';
|
||||
import ReportHeatmap from './Heatmap.vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useLiveRefresh } from 'dashboard/composables/useLiveRefresh';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const uiFlags = useMapGetter('getOverviewUIFlags');
|
||||
const accountConversationHeatmap = useMapGetter(
|
||||
'getAccountConversationHeatmapData'
|
||||
);
|
||||
const { t } = useI18n();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_7_DAYS'),
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
label: t('REPORT.DATE_RANGE_OPTIONS.LAST_30_DAYS'),
|
||||
value: 29,
|
||||
},
|
||||
];
|
||||
|
||||
const selectedDays = ref(6);
|
||||
|
||||
const selectedDayFilter = computed(() =>
|
||||
menuItems.find(menuItem => menuItem.value === selectedDays.value)
|
||||
);
|
||||
|
||||
const downloadHeatmapData = () => {
|
||||
const to = endOfDay(new Date());
|
||||
store.dispatch('downloadAccountConversationHeatmap', {
|
||||
daysBefore: selectedDays.value,
|
||||
to: getUnixTime(to),
|
||||
});
|
||||
};
|
||||
const [showDropdown, toggleDropdown] = useToggle();
|
||||
const fetchHeatmapData = () => {
|
||||
if (uiFlags.value.isFetchingAccountConversationsHeatmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
let to = endOfDay(new Date());
|
||||
let from = startOfDay(subDays(to, Number(selectedDays.value)));
|
||||
|
||||
store.dispatch('fetchAccountConversationHeatmap', {
|
||||
metric: 'conversations_count',
|
||||
from: getUnixTime(from),
|
||||
to: getUnixTime(to),
|
||||
groupBy: 'hour',
|
||||
businessHours: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAction = ({ value }) => {
|
||||
toggleDropdown(false);
|
||||
selectedDays.value = value;
|
||||
fetchHeatmapData();
|
||||
};
|
||||
|
||||
const { startRefetching } = useLiveRefresh(fetchHeatmapData);
|
||||
|
||||
onMounted(() => {
|
||||
fetchHeatmapData();
|
||||
startRefetching();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row flex-wrap max-w-full">
|
||||
<MetricCard :header="$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.HEADER')">
|
||||
<template #control>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
sm
|
||||
slate
|
||||
faded
|
||||
:label="selectedDayFilter.label"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showDropdown"
|
||||
:menu-items="menuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full"
|
||||
@action="handleAction($event)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
sm
|
||||
slate
|
||||
faded
|
||||
:label="t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT')"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="downloadHeatmapData"
|
||||
/>
|
||||
</template>
|
||||
<ReportHeatmap
|
||||
:heatmap-data="accountConversationHeatmap"
|
||||
:number-of-rows="selectedDays + 1"
|
||||
:is-loading="uiFlags.isFetchingAccountConversationsHeatmap"
|
||||
/>
|
||||
</MetricCard>
|
||||
</div>
|
||||
</template>
|
||||
+14
-22
@@ -1,26 +1,20 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
name: 'MetricCard',
|
||||
components: {
|
||||
Spinner,
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
props: {
|
||||
header: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,9 +40,7 @@ export default {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="transition-opacity duration-200 ease-in-out opacity-20 hover:opacity-100 flex flex-row items-center justify-end gap-2"
|
||||
>
|
||||
<div class="flex flex-row items-center justify-end gap-2">
|
||||
<slot name="control" />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -4,10 +4,7 @@ import Report from '../../api/reports';
|
||||
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||
import {
|
||||
reconcileHeatmapData,
|
||||
clampDataBetweenTimeline,
|
||||
} from 'shared/helpers/ReportsDataHelper';
|
||||
import { clampDataBetweenTimeline } from 'shared/helpers/ReportsDataHelper';
|
||||
|
||||
const state = {
|
||||
fetchingStatus: false,
|
||||
@@ -114,11 +111,6 @@ export const actions = {
|
||||
let { data } = heatmapData;
|
||||
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
||||
|
||||
data = reconcileHeatmapData(
|
||||
data,
|
||||
state.overview.accountConversationHeatmap
|
||||
);
|
||||
|
||||
commit(types.default.SET_HEATMAP_DATA, data);
|
||||
commit(types.default.TOGGLE_HEATMAP_LOADING, false);
|
||||
});
|
||||
@@ -234,7 +226,7 @@ export const actions = {
|
||||
});
|
||||
},
|
||||
downloadAccountConversationHeatmap(_, reportObj) {
|
||||
Report.getConversationTrafficCSV()
|
||||
Report.getConversationTrafficCSV({ daysBefore: reportObj.daysBefore })
|
||||
.then(response => {
|
||||
downloadCsvFile(
|
||||
generateFileName({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DYTE_MEETING_LINK = 'https://app.dyte.in/meeting/stage/';
|
||||
const DYTE_MEETING_LINK = 'https://app.dyte.io/v2/meeting';
|
||||
|
||||
export const buildDyteURL = (roomName, dyteAuthToken) => {
|
||||
return `${DYTE_MEETING_LINK}${roomName}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`;
|
||||
export const buildDyteURL = dyteAuthToken => {
|
||||
return `${DYTE_MEETING_LINK}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`;
|
||||
};
|
||||
|
||||
@@ -14,10 +14,6 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
meetingData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
|
||||
@@ -28,18 +24,18 @@ export default {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
meetingLink() {
|
||||
return buildDyteURL(this.meetingData.room_name, this.dyteAuthToken);
|
||||
return buildDyteURL(this.dyteAuthToken);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async joinTheCall() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const { data: { authResponse: { authToken = '' } = {} } = {} } =
|
||||
await IntegrationAPIClient.addParticipantToDyteMeeting(
|
||||
this.messageId
|
||||
);
|
||||
this.dyteAuthToken = authToken;
|
||||
const response = await IntegrationAPIClient.addParticipantToDyteMeeting(
|
||||
this.messageId
|
||||
);
|
||||
const { data: { token } = {} } = response;
|
||||
this.dyteAuthToken = token;
|
||||
} catch (error) {
|
||||
// Ignore Error for now
|
||||
} finally {
|
||||
|
||||
+11
-17
@@ -1,9 +1,10 @@
|
||||
class Dyte
|
||||
BASE_URL = 'https://api.cluster.dyte.in/v1'.freeze
|
||||
BASE_URL = 'https://api.dyte.io/v2'.freeze
|
||||
API_KEY_HEADER = 'Authorization'.freeze
|
||||
PRESET_NAME = 'group_call_host'.freeze
|
||||
|
||||
def initialize(organization_id, api_key)
|
||||
@api_key = api_key
|
||||
@api_key = Base64.strict_encode64("#{organization_id}:#{api_key}")
|
||||
@organization_id = organization_id
|
||||
|
||||
raise ArgumentError, 'Missing Credentials' if @api_key.blank? || @organization_id.blank?
|
||||
@@ -11,15 +12,9 @@ class Dyte
|
||||
|
||||
def create_a_meeting(title)
|
||||
payload = {
|
||||
'title': title,
|
||||
'authorization': {
|
||||
'waitingRoom': false,
|
||||
'closed': false
|
||||
},
|
||||
'recordOnStart': false,
|
||||
'liveStreamOnStart': false
|
||||
'title': title
|
||||
}
|
||||
path = "organizations/#{@organization_id}/meeting"
|
||||
path = 'meetings'
|
||||
response = post(path, payload)
|
||||
process_response(response)
|
||||
end
|
||||
@@ -28,13 +23,12 @@ class Dyte
|
||||
raise ArgumentError, 'Missing information' if meeting_id.blank? || client_id.blank? || name.blank? || avatar_url.blank?
|
||||
|
||||
payload = {
|
||||
'clientSpecificId': client_id.to_s,
|
||||
'userDetails': {
|
||||
'name': name,
|
||||
'picture': avatar_url
|
||||
}
|
||||
'custom_participant_id': client_id.to_s,
|
||||
'name': name,
|
||||
'picture': avatar_url,
|
||||
'preset_name': PRESET_NAME
|
||||
}
|
||||
path = "organizations/#{@organization_id}/meetings/#{meeting_id}/participant"
|
||||
path = "meetings/#{meeting_id}/participants"
|
||||
response = post(path, payload)
|
||||
process_response(response)
|
||||
end
|
||||
@@ -50,7 +44,7 @@ class Dyte
|
||||
def post(path, payload)
|
||||
HTTParty.post(
|
||||
"#{BASE_URL}/#{path}", {
|
||||
headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' },
|
||||
headers: { API_KEY_HEADER => "Basic #{@api_key}", 'Content-Type' => 'application/json' },
|
||||
body: payload.to_json
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ class Integrations::Dyte::ProcessorService
|
||||
|
||||
return response if response[:error].present?
|
||||
|
||||
meeting = response['meeting']
|
||||
meeting = response
|
||||
message = create_a_dyte_integration_message(meeting, title, agent)
|
||||
message.push_event_data
|
||||
end
|
||||
@@ -29,8 +29,7 @@ class Integrations::Dyte::ProcessorService
|
||||
content_attributes: {
|
||||
type: 'dyte',
|
||||
data: {
|
||||
meeting_id: meeting['id'],
|
||||
room_name: meeting['roomName']
|
||||
meeting_id: meeting['id']
|
||||
}
|
||||
},
|
||||
sender: agent
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@
|
||||
"countries-and-timezones": "^3.6.0",
|
||||
"date-fns": "2.21.1",
|
||||
"date-fns-tz": "^1.3.3",
|
||||
"dompurify": "3.1.6",
|
||||
"dompurify": "3.2.4",
|
||||
"flag-icons": "^7.2.3",
|
||||
"floating-vue": "^5.2.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
|
||||
Generated
+16
-14
@@ -119,8 +119,8 @@ importers:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.8(date-fns@2.21.1)
|
||||
dompurify:
|
||||
specifier: 3.1.6
|
||||
version: 3.1.6
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4
|
||||
flag-icons:
|
||||
specifier: ^7.2.3
|
||||
version: 7.2.3
|
||||
@@ -1737,8 +1737,8 @@ packages:
|
||||
'@types/node@22.7.0':
|
||||
resolution: {integrity: sha512-MOdOibwBs6KW1vfqz2uKMlxq5xAfAZ98SZjO8e3XnAbFnTJtAspqhWk7hrdSAs9/Y14ZWMiy7/MxMUzAOadYEw==}
|
||||
|
||||
'@types/trusted-types@2.0.2':
|
||||
resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/web-bluetooth@0.0.20':
|
||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||
@@ -2497,8 +2497,8 @@ packages:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.1.6:
|
||||
resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==}
|
||||
dompurify@3.2.4:
|
||||
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
|
||||
|
||||
domutils@3.1.0:
|
||||
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||
@@ -4172,8 +4172,8 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
readdirp@4.1.1:
|
||||
resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==}
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
recordrtc@5.6.2:
|
||||
@@ -6594,7 +6594,7 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/trusted-types@2.0.2': {}
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.20': {}
|
||||
|
||||
@@ -7225,7 +7225,7 @@ snapshots:
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.1
|
||||
readdirp: 4.1.2
|
||||
optional: true
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
@@ -7480,7 +7480,9 @@ snapshots:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.1.6: {}
|
||||
dompurify@3.2.4:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
domutils@3.1.0:
|
||||
dependencies:
|
||||
@@ -8623,7 +8625,7 @@ snapshots:
|
||||
|
||||
lit-html@2.8.0:
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.2
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
lit@2.2.6:
|
||||
dependencies:
|
||||
@@ -9456,7 +9458,7 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
readdirp@4.1.1:
|
||||
readdirp@4.1.2:
|
||||
optional: true
|
||||
|
||||
recordrtc@5.6.2: {}
|
||||
@@ -10204,7 +10206,7 @@ snapshots:
|
||||
|
||||
vue-dompurify-html@5.1.0(vue@3.5.12(typescript@5.6.2)):
|
||||
dependencies:
|
||||
dompurify: 3.1.6
|
||||
dompurify: 3.2.4
|
||||
vue: 3.5.12(typescript@5.6.2)
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@8.57.0):
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
|
||||
<title>woot-log</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="woot-log" fill-rule="nonzero">
|
||||
<circle id="Oval" fill="#47A7F6" cx="256" cy="256" r="256"></circle>
|
||||
<path d="M362.807947,368.807947 L244.122956,368.807947 C178.699407,368.807947 125.456954,315.561812 125.456954,250.12177 C125.456954,184.703089 178.699407,131.456954 244.124143,131.456954 C309.565494,131.456954 362.807947,184.703089 362.807947,250.12177 L362.807947,368.807947 Z" id="Fill-1" stroke="#FFFFFF" stroke-width="6" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#woot-logo-clip-2342424e23u32098)">
|
||||
<path
|
||||
d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z"
|
||||
fill="#2781F6"
|
||||
/>
|
||||
<path
|
||||
d="M11.4172 11.4172H7.70831C5.66383 11.4172 4 9.75328 4 7.70828C4 5.66394 5.66383 4 7.70835 4C9.75339 4 11.4172 5.66394 11.4172 7.70828V11.4172Z"
|
||||
fill="white"
|
||||
stroke="white"
|
||||
stroke-width="0.1875"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="woot-logo-clip-2342424e23u32098">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 709 B |
@@ -39,10 +39,10 @@ RSpec.describe 'Dyte Integration API', type: :request do
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is a success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { meeting: { id: 'meeting_id', roomName: 'room_name' } } }.to_json,
|
||||
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
@@ -62,7 +62,7 @@ RSpec.describe 'Dyte Integration API', type: :request do
|
||||
|
||||
context 'when it is an agent with inbox access and the Dyte API is errored' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 422,
|
||||
body: { success: false, data: { message: 'Title is required' } }.to_json,
|
||||
@@ -112,24 +112,24 @@ RSpec.describe 'Dyte Integration API', type: :request do
|
||||
|
||||
context 'when it is an agent with inbox access and message_type is integrations' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns authResponse' do
|
||||
it 'returns auth_token' do
|
||||
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
|
||||
params: { message_id: integration_message.id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['authResponse']).to eq(
|
||||
expect(response_body).to eq(
|
||||
{
|
||||
'userAdded' => true, 'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -46,15 +46,15 @@ RSpec.describe '/api/v1/widget/integrations/dyte', type: :request do
|
||||
|
||||
context 'when message is an integration message' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns authResponse' do
|
||||
it 'returns auth_token' do
|
||||
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token, message_id: integration_message.id },
|
||||
@@ -62,9 +62,9 @@ RSpec.describe '/api/v1/widget/integrations/dyte', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['authResponse']).to eq(
|
||||
expect(response_body).to eq(
|
||||
{
|
||||
'userAdded' => true, 'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -11,23 +11,23 @@ describe Dyte do
|
||||
context 'when create_a_meeting is called' do
|
||||
context 'when API response is success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { meeting: { id: 'meeting_id' } } }.to_json,
|
||||
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns api response' do
|
||||
response = dyte_client.create_a_meeting('title_of_the_meeting')
|
||||
expect(response).to eq({ 'meeting' => { 'id' => 'meeting_id' } })
|
||||
expect(response).to eq({ 'id' => 'meeting_id' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API response is invalid' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(status: 422, body: { message: 'Title is required' }.to_json, headers: headers)
|
||||
end
|
||||
|
||||
@@ -47,23 +47,23 @@ describe Dyte do
|
||||
|
||||
context 'when API response is success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns api response' do
|
||||
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
|
||||
expect(response).to eq({ 'authResponse' => { 'userAdded' => true, 'id' => 'random_uuid', 'auth_token' => 'json-web-token' } })
|
||||
expect(response).to eq({ 'id' => 'random_uuid', 'auth_token' => 'json-web-token' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API response is invalid' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(status: 422, body: { message: 'Meeting ID is invalid' }.to_json, headers: headers)
|
||||
end
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ describe Integrations::Dyte::ProcessorService do
|
||||
describe '#create_a_meeting' do
|
||||
context 'when the API response is success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { meeting: { id: 'meeting_id', roomName: 'room_name' } } }.to_json,
|
||||
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
@@ -32,7 +32,7 @@ describe Integrations::Dyte::ProcessorService do
|
||||
|
||||
context 'when the API response is errored' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings')
|
||||
.to_return(
|
||||
status: 422,
|
||||
body: { success: false, data: { message: 'Title is required' } }.to_json,
|
||||
@@ -51,17 +51,17 @@ describe Integrations::Dyte::ProcessorService do
|
||||
describe '#add_participant_to_meeting' do
|
||||
context 'when the API response is success' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
|
||||
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
|
||||
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
|
||||
headers: headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'return the authResponse' do
|
||||
response = processor.add_participant_to_meeting('m_id', agent)
|
||||
expect(response[:authResponse]).not_to be_nil
|
||||
expect(response).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user