Merge branch 'develop' into chore/conv-card-bulk-checkbox

This commit is contained in:
Sivin Varghese
2025-02-20 08:57:36 +05:30
committed by GitHub
27 changed files with 324 additions and 221 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client', '~> 2.2.0'
gem 'slack-ruby-client', '~> 2.5.1'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
gem 'grpc'
+10 -7
View File
@@ -272,7 +272,8 @@ GEM
googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.1)
gli (2.21.1)
gli (2.22.2)
ostruct
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.3)
@@ -497,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)
@@ -537,6 +538,7 @@ GEM
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.1)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
@@ -745,12 +747,13 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (2.2.0)
slack-ruby-client (2.5.1)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
logger
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -951,7 +954,7 @@ DEPENDENCIES
sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
slack-ruby-client (~> 2.5.1)
spring
spring-watcher-listen
squasher
@@ -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)
+2 -2
View File
@@ -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 } = 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(meetingData.value.messageId);
dyteAuthToken.value = authToken;
const { data: { token } = {} } = await DyteAPI.addParticipantToMeeting(
id.value
);
dyteAuthToken.value = token;
} catch (err) {
useAlert(t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
@@ -53,7 +49,7 @@ const action = computed(() => ({
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.MEETING"
:action="action"
>
<div v-if="!sender" class="text-n-slate-12 text-sm truncate">
<div v-if="!sender" class="text-sm truncate text-n-slate-12">
<!-- Added as a fallback, where the sender is not available (Deleted) -->
<!-- Will show the content, if senderName in BaseAttachment.vue is empty -->
{{ content }}
@@ -64,7 +60,7 @@ const action = computed(() => ({
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<button
class="bg-n-solid-3 px-4 py-2 rounded-lg text-sm"
class="px-4 py-2 text-sm rounded-lg bg-n-solid-3"
@click="leaveTheRoom"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
@@ -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
@@ -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)]"
>
@@ -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>
@@ -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
View File
@@ -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
}
)
+2 -3
View File
@@ -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
@@ -119,12 +119,14 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
def upload_file
return unless message.attachments.first.with_attached_file?
result = slack_client.files_upload({
channels: hook.reference_id,
result = slack_client.files_upload_v2(
filename: message.attachments.first.file.filename,
content: message.attachments.first.file.download,
initial_comment: 'Attached File!',
thread_ts: conversation.identifier
}.merge(file_information))
Rails.logger.info(result)
thread_ts: conversation.identifier,
channel_id: hook.reference_id
)
Rails.logger.info "slack_upload_result: #{result}"
end
def file_type
+1 -1
View File
@@ -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",
+16 -14
View File
@@ -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):
+25 -12
View File
@@ -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
+8 -8
View File
@@ -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
@@ -162,15 +162,13 @@ describe Integrations::Slack::SendOnSlackService do
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
expect(slack_client).to receive(:files_upload).with(hash_including(
channels: hook.reference_id,
thread_ts: conversation.identifier,
initial_comment: 'Attached File!',
filetype: 'png',
content: anything,
filename: attachment.file.filename,
title: attachment.file.filename
)).and_return(file_attachment)
expect(slack_client).to receive(:files_upload_v2).with(
filename: attachment.file.filename,
content: anything,
channel_id: hook.reference_id,
thread_ts: conversation.identifier,
initial_comment: 'Attached File!'
).and_return(file_attachment)
message.save!