Merge branch 'develop' into feat/reload-banner-chat-list
This commit is contained in:
commit
d8a39bb0d8
57 changed files with 1095 additions and 159 deletions
114
Gemfile.lock
114
Gemfile.lock
|
@ -9,63 +9,63 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actioncable (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actionmailbox (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actionmailer (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actionpack (6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actiontext (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
actionview (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activejob (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activerecord (6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activemodel (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord (6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord-import (1.3.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activestorage (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.6)
|
||||
activesupport (6.1.4.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -135,7 +135,7 @@ GEM
|
|||
byebug (11.1.3)
|
||||
climate_control (1.0.1)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.2)
|
||||
commonmarker (0.23.4)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
|
@ -303,7 +303,7 @@ GEM
|
|||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.1)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.5)
|
||||
|
@ -419,29 +419,29 @@ GEM
|
|||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.4.6)
|
||||
actioncable (= 6.1.4.6)
|
||||
actionmailbox (= 6.1.4.6)
|
||||
actionmailer (= 6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actiontext (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
rails (6.1.4.7)
|
||||
actioncable (= 6.1.4.7)
|
||||
actionmailbox (= 6.1.4.7)
|
||||
actionmailer (= 6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actiontext (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.6)
|
||||
railties (= 6.1.4.7)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
railties (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
|
@ -574,7 +574,7 @@ GEM
|
|||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.2)
|
||||
sprockets (4.0.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.2)
|
||||
|
@ -751,4 +751,4 @@ RUBY VERSION
|
|||
ruby 3.0.2p107
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.25
|
||||
2.3.8
|
||||
|
|
|
@ -3,7 +3,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
||||
|
||||
def index
|
||||
@automation_rules = Current.account.automation_rules.active
|
||||
@automation_rules = Current.account.automation_rules
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -32,7 +32,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id,
|
||||
:name, :description, :event_name, :account_id, :active,
|
||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||
actions: [:action_name, { action_params: [] }]
|
||||
)
|
||||
|
|
|
@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
|
|
|
@ -3,7 +3,7 @@ module FileTypeHelper
|
|||
def file_type(content_type)
|
||||
return :image if image_file?(content_type)
|
||||
return :video if video_file?(content_type)
|
||||
return :audio if content_type.include?('audio/')
|
||||
return :audio if content_type&.include?('audio/')
|
||||
|
||||
:file
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
|||
.post('auth/sign_in', creds)
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve();
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
|
|
|
@ -6,15 +6,21 @@ class CSATReportsAPI extends ApiClient {
|
|||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to } = {}) {
|
||||
get({ page, from, to, user_ids } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, since: from, until: to, sort: '-created_at' },
|
||||
params: {
|
||||
page,
|
||||
since: from,
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to } = {}) {
|
||||
getMetrics({ from, to, user_ids } = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to },
|
||||
params: { since: from, until: to, user_ids },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton';
|
|||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
|
@ -45,6 +46,7 @@ const WootUIKit = {
|
|||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmModal,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="beta"
|
||||
>Beta
|
||||
>
|
||||
{{ $t('SIDEBAR.BETA') }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
|
@ -233,7 +234,7 @@ export default {
|
|||
padding-left: var(--space-smaller) !important;
|
||||
margin-left: var(--space-half) !important;
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-mini);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px;
|
||||
border: 1px solid transparent;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
</div>
|
||||
<div class="remove-file-wrap">
|
||||
<woot-button
|
||||
v-if="!isTypeAudio(attachment.resource)"
|
||||
class="remove--attachment clear secondary"
|
||||
icon="dismiss"
|
||||
@click="() => onRemoveAttachment(index)"
|
||||
|
@ -58,6 +59,10 @@ export default {
|
|||
const type = file.content_type || file.type;
|
||||
return type.includes('image');
|
||||
},
|
||||
isTypeAudio(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('audio');
|
||||
},
|
||||
fileName(file) {
|
||||
return file.filename || file.name;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
<template>
|
||||
<div class="audio-wave-wrapper">
|
||||
<div id="audio-wave"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WaveSurfer from 'wavesurfer.js';
|
||||
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
||||
import RecordRTC from 'recordrtc';
|
||||
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
||||
|
||||
WaveSurfer.microphone = MicrophonePlugin;
|
||||
|
||||
export default {
|
||||
name: 'WootAudioRecorder',
|
||||
mixins: [inboxMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
wavesurfer: false,
|
||||
recorder: false,
|
||||
recordingInterval: false,
|
||||
recordingDateStarted: new Date().getTime(),
|
||||
timeDuration: '00:00',
|
||||
initialTimeDuration: '00:00',
|
||||
options: {
|
||||
container: '#audio-wave',
|
||||
backend: 'WebAudio',
|
||||
interact: true,
|
||||
cursorWidth: 1,
|
||||
plugins: [
|
||||
WaveSurfer.microphone.create({
|
||||
bufferSize: 4096,
|
||||
numberOfInputChannels: 1,
|
||||
numberOfOutputChannels: 1,
|
||||
constraints: {
|
||||
video: false,
|
||||
audio: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
optionsRecorder: {
|
||||
type: 'audio',
|
||||
mimeType: 'audio/wav',
|
||||
disableLogs: true,
|
||||
recorderType: RecordRTC.StereoAudioRecorder,
|
||||
sampleRate: 44100,
|
||||
numberOfAudioChannels: 2,
|
||||
checkForInactiveTracks: true,
|
||||
bufferSize: 4096,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isRecording() {
|
||||
if (this.recorder) {
|
||||
return this.recorder.getState() === 'recording';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.wavesurfer = WaveSurfer.create(this.options);
|
||||
this.wavesurfer.on('play', this.playingRecorder);
|
||||
this.wavesurfer.on('pause', this.pausedRecorder);
|
||||
this.wavesurfer.microphone.on('deviceReady', this.startRecording);
|
||||
this.wavesurfer.microphone.on('deviceError', this.deviceError);
|
||||
this.wavesurfer.microphone.start();
|
||||
this.fireStateRecorderTimerChanged(this.initialTimeDuration);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.recorder) {
|
||||
this.recorder.destroy();
|
||||
}
|
||||
if (this.wavesurfer) {
|
||||
this.wavesurfer.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startRecording(stream) {
|
||||
this.recorder = RecordRTC(stream, this.optionsRecorder);
|
||||
this.recorder.onStateChanged = this.onStateRecorderChanged;
|
||||
this.recorder.startRecording();
|
||||
},
|
||||
stopAudioRecording() {
|
||||
if (this.isRecording) {
|
||||
this.recorder.stopRecording(() => {
|
||||
this.wavesurfer.microphone.stopDevice();
|
||||
this.wavesurfer.loadBlob(this.recorder.getBlob());
|
||||
this.wavesurfer.stop();
|
||||
this.fireRecorderBlob(this.getAudioFile());
|
||||
});
|
||||
}
|
||||
},
|
||||
getAudioFile() {
|
||||
if (this.hasAudio()) {
|
||||
return new File([this.recorder.getBlob()], this.getAudioFileName(), {
|
||||
type: 'audio/wav',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasAudio() {
|
||||
return !(this.isRecording || this.wavesurfer.isPlaying());
|
||||
},
|
||||
playingRecorder() {
|
||||
this.fireStateRecorderChanged('playing');
|
||||
},
|
||||
pausedRecorder() {
|
||||
this.fireStateRecorderChanged('paused');
|
||||
},
|
||||
deviceError(err) {
|
||||
if (
|
||||
err?.name &&
|
||||
(err.name.toLowerCase().includes('notallowederror') ||
|
||||
err.name.toLowerCase().includes('permissiondeniederror'))
|
||||
) {
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
||||
);
|
||||
this.fireStateRecorderChanged('notallowederror');
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
onStateRecorderChanged(state) {
|
||||
// recording stopped inactive destroyed
|
||||
switch (state) {
|
||||
case 'recording':
|
||||
this.timerDurationChanged();
|
||||
break;
|
||||
case 'stopped':
|
||||
this.timerDurationChanged();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.fireStateRecorderChanged(state);
|
||||
},
|
||||
timerDurationChanged() {
|
||||
if (this.isRecording) {
|
||||
this.recordingInterval = setInterval(() => {
|
||||
this.calculateTimeDuration(
|
||||
(new Date().getTime() - this.recordingDateStarted) / 1000
|
||||
);
|
||||
this.fireStateRecorderTimerChanged(this.timeDuration);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(this.recordingInterval);
|
||||
}
|
||||
},
|
||||
calculateTimeDuration(secs) {
|
||||
let hr = Math.floor(secs / 3600);
|
||||
let min = Math.floor((secs - hr * 3600) / 60);
|
||||
let sec = Math.floor(secs - hr * 3600 - min * 60);
|
||||
if (min < 10) {
|
||||
min = '0' + min;
|
||||
}
|
||||
if (sec < 10) {
|
||||
sec = '0' + sec;
|
||||
}
|
||||
if (hr <= 0) {
|
||||
this.timeDuration = min + ':' + sec;
|
||||
} else {
|
||||
if (hr < 10) {
|
||||
hr = '0' + hr;
|
||||
}
|
||||
this.timeDuration = hr + ':' + min + ':' + sec;
|
||||
}
|
||||
},
|
||||
playPause() {
|
||||
this.wavesurfer.playPause();
|
||||
},
|
||||
fireRecorderBlob(blob) {
|
||||
this.$emit('recorder-blob', {
|
||||
name: blob.name,
|
||||
type: blob.type,
|
||||
size: blob.size,
|
||||
file: blob,
|
||||
});
|
||||
},
|
||||
fireStateRecorderChanged(state) {
|
||||
this.$emit('state-recorder-changed', state);
|
||||
},
|
||||
fireStateRecorderTimerChanged(duration) {
|
||||
this.$emit('state-recorder-timer-changed', duration);
|
||||
},
|
||||
getAudioFileName() {
|
||||
const d = new Date();
|
||||
return `audio-${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${this.getRandomString()}.wav`;
|
||||
},
|
||||
getRandomString() {
|
||||
if (
|
||||
window.crypto &&
|
||||
window.crypto.getRandomValues &&
|
||||
navigator.userAgent.indexOf('Safari') === -1
|
||||
) {
|
||||
let a = window.crypto.getRandomValues(new Uint32Array(3));
|
||||
let token = '';
|
||||
for (let i = 0, l = a.length; i < l; i += 1) {
|
||||
token += a[i].toString(36);
|
||||
}
|
||||
return token.toLowerCase();
|
||||
}
|
||||
return (Math.random() * new Date().getTime())
|
||||
.toString(36)
|
||||
.replace(/\./g, '');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.audio-wave-wrapper {
|
||||
min-height: 8rem;
|
||||
max-height: 12rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -11,7 +11,6 @@
|
|||
size="small"
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
|
||||
<!-- ensure the same validations for attachment types are implemented in backend models as well -->
|
||||
<file-upload
|
||||
ref="upload"
|
||||
|
@ -49,6 +48,27 @@
|
|||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showAudioRecorderButton"
|
||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||
emoji="🎤"
|
||||
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
@click="toggleAudioRecorder"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showAudioPlayStopButton"
|
||||
:icon="audioRecorderPlayStopIcon"
|
||||
emoji="🎤"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleAudioRecorderPlayPause"
|
||||
>
|
||||
<span>{{ recordingAudioDurationText }}</span>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showMessageSignatureButton"
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
|
@ -126,6 +146,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
recordingAudioDurationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
|
@ -134,6 +158,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showAudioRecorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onFileUpload: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
|
@ -146,6 +174,22 @@ export default {
|
|||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
toggleAudioRecorder: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
toggleAudioRecorderPlayPause: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
isRecordingAudio: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recordingAudioState: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isSendDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -192,9 +236,28 @@ export default {
|
|||
showAttachButton() {
|
||||
return this.showFileUpload || this.isNote;
|
||||
},
|
||||
showAudioRecorderButton() {
|
||||
return this.showAudioRecorder;
|
||||
},
|
||||
showAudioPlayStopButton() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
},
|
||||
allowedFileTypes() {
|
||||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
audioRecorderPlayStopIcon() {
|
||||
switch (this.recordingAudioState) {
|
||||
// playing paused recording stopped inactive destroyed
|
||||
case 'playing':
|
||||
return 'microphone-pause';
|
||||
case 'paused':
|
||||
return 'microphone-play';
|
||||
case 'stopped':
|
||||
return 'microphone-play';
|
||||
default:
|
||||
return 'microphone-stop';
|
||||
}
|
||||
},
|
||||
showMessageSignatureButton() {
|
||||
return !this.isPrivate && this.isAnEmailChannel;
|
||||
},
|
||||
|
|
|
@ -141,6 +141,12 @@ export default {
|
|||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'text':
|
||||
return 'plain_text';
|
||||
case 'list':
|
||||
return 'search_select';
|
||||
case 'checkbox':
|
||||
return 'search_select';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
|
@ -159,6 +165,47 @@ export default {
|
|||
},
|
||||
getDropdownValues(type) {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
](this.attributeModel);
|
||||
|
||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type &&
|
||||
attr.attribute_display_type === 'checkbox'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeCheckbox) {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeList) {
|
||||
return allCustomAttributes
|
||||
.find(attr => attr.attribute_key === type)
|
||||
.attribute_values.map(item => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'status':
|
||||
return [
|
||||
|
|
|
@ -34,8 +34,15 @@
|
|||
:cc-emails.sync="ccEmails"
|
||||
:bcc-emails.sync="bccEmails"
|
||||
/>
|
||||
<woot-audio-recorder
|
||||
v-if="showAudioRecorderEditor"
|
||||
ref="audioRecorderInput"
|
||||
@state-recorder-timer-changed="onStateRecorderTimerChanged"
|
||||
@state-recorder-changed="onStateRecorderChanged"
|
||||
@recorder-blob="onRecorderBlob"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!showRichContentEditor"
|
||||
v-else-if="!showRichContentEditor"
|
||||
ref="messageInput"
|
||||
v-model="message"
|
||||
class="input"
|
||||
|
@ -90,10 +97,16 @@
|
|||
:send-button-text="replyButtonLabel"
|
||||
:on-file-upload="onFileUpload"
|
||||
:show-file-upload="showFileUpload"
|
||||
:show-audio-recorder="showAudioRecorder"
|
||||
:toggle-emoji-picker="toggleEmojiPicker"
|
||||
:toggle-audio-recorder="toggleAudioRecorder"
|
||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||
:show-emoji-picker="showEmojiPicker"
|
||||
:on-send="sendMessage"
|
||||
:is-send-disabled="isReplyButtonDisabled"
|
||||
:recording-audio-duration-text="recordingAudioDuration"
|
||||
:recording-audio-state="recordingAudioState"
|
||||
:is-recording-audio="isRecordingAudio"
|
||||
:set-format-mode="setFormatMode"
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-format-mode="showRichContentEditor"
|
||||
|
@ -120,6 +133,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto
|
|||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||
|
@ -147,6 +161,7 @@ export default {
|
|||
ReplyEmailHead,
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
WootAudioRecorder,
|
||||
Banner,
|
||||
},
|
||||
mixins: [
|
||||
|
@ -177,6 +192,9 @@ export default {
|
|||
showEmojiPicker: false,
|
||||
showMentions: false,
|
||||
attachedFiles: [],
|
||||
isRecordingAudio: false,
|
||||
recordingAudioState: '',
|
||||
recordingAudioDuration: '',
|
||||
isUploading: false,
|
||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||
mentionSearchKey: '',
|
||||
|
@ -271,7 +289,7 @@ export default {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (this.hasAttachments) return false;
|
||||
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
||||
|
||||
return (
|
||||
this.isMessageEmpty ||
|
||||
|
@ -294,7 +312,7 @@ export default {
|
|||
if (this.isAWhatsappChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
if (this.isATwilioSMSChannel) {
|
||||
if (this.isASmsInbox) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||
}
|
||||
if (this.isATwitterInbox) {
|
||||
|
@ -311,7 +329,7 @@ export default {
|
|||
this.isAWhatsappChannel ||
|
||||
this.isAPIInbox ||
|
||||
this.isAnEmailChannel ||
|
||||
this.isATwilioSMSChannel ||
|
||||
this.isASmsInbox ||
|
||||
this.isATelegramChannel
|
||||
);
|
||||
},
|
||||
|
@ -333,9 +351,21 @@ export default {
|
|||
hasAttachments() {
|
||||
return this.attachedFiles.length;
|
||||
},
|
||||
hasRecordedAudio() {
|
||||
return (
|
||||
this.$refs.audioRecorderInput &&
|
||||
this.$refs.audioRecorderInput.hasAudio()
|
||||
);
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||
},
|
||||
showAudioRecorder() {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
},
|
||||
showAudioRecorderEditor() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
},
|
||||
isOnPrivateNote() {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
|
@ -524,6 +554,9 @@ export default {
|
|||
|
||||
if (canReply || this.isAWhatsappChannel) this.replyType = mode;
|
||||
if (this.showRichContentEditor) {
|
||||
if (this.isRecordingAudio) {
|
||||
this.toggleAudioRecorder();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||
|
@ -536,10 +569,26 @@ export default {
|
|||
this.attachedFiles = [];
|
||||
this.ccEmails = '';
|
||||
this.bccEmails = '';
|
||||
this.isRecordingAudio = false;
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
toggleAudioRecorder() {
|
||||
this.isRecordingAudio = !this.isRecordingAudio;
|
||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||
if (!this.isRecordingAudio) {
|
||||
this.clearMessage();
|
||||
}
|
||||
},
|
||||
toggleAudioRecorderPlayPause() {
|
||||
if (this.isRecordingAudio && !this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopAudioRecording();
|
||||
} else if (this.isRecordingAudio && this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
if (this.showEmojiPicker) {
|
||||
this.toggleEmojiPicker();
|
||||
|
@ -560,6 +609,20 @@ export default {
|
|||
onFocus() {
|
||||
this.isFocused = true;
|
||||
},
|
||||
onStateRecorderTimerChanged(time) {
|
||||
this.recordingAudioDuration = time;
|
||||
},
|
||||
onStateRecorderChanged(state) {
|
||||
this.recordingAudioState = state;
|
||||
if (state.includes('notallowederror')) {
|
||||
this.toggleAudioRecorder();
|
||||
}
|
||||
},
|
||||
onRecorderBlob(file) {
|
||||
if (file) {
|
||||
this.onFileUpload(file);
|
||||
}
|
||||
},
|
||||
toggleTyping(status) {
|
||||
const conversationId = this.currentChat.id;
|
||||
const isPrivate = this.isPrivate;
|
||||
|
@ -701,6 +764,8 @@ export default {
|
|||
justify-content: space-between;
|
||||
border: 1px dashed var(--s-100);
|
||||
border-radius: var(--border-radius-small);
|
||||
max-height: 8vh;
|
||||
overflow: auto;
|
||||
|
||||
&:hover {
|
||||
background: var(--s-25);
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="cancel">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header :header-title="title"> </woot-modal-header>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns">
|
||||
<p>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-button @click="confirm">
|
||||
{{ confirmLabel }}
|
||||
</woot-button>
|
||||
<button class="button clear" @click="cancel">
|
||||
{{ cancelLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
<script>
|
||||
import Modal from '../../Modal';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'This is a title',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'This is your description',
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: 'Yes',
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: 'No',
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
show: false,
|
||||
resolvePromise: undefined,
|
||||
rejectPromise: undefined,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
showConfirmation() {
|
||||
this.show = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
},
|
||||
confirm() {
|
||||
this.resolvePromise(true);
|
||||
this.show = false;
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.resolvePromise(false);
|
||||
this.show = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,10 +1,22 @@
|
|||
import queryString from 'query-string';
|
||||
import { DEFAULT_REDIRECT_URL } from '../constants';
|
||||
|
||||
export const frontendURL = (path, params) => {
|
||||
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
|
||||
return `/app/${path}${stringifiedParams}`;
|
||||
};
|
||||
|
||||
export const getLoginRedirectURL = (ssoAccountId, user) => {
|
||||
const { accounts = [] } = user || {};
|
||||
const ssoAccount = accounts.find(
|
||||
account => account.id === Number(ssoAccountId)
|
||||
);
|
||||
if (ssoAccount) {
|
||||
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
|
||||
}
|
||||
return DEFAULT_REDIRECT_URL;
|
||||
};
|
||||
|
||||
export const conversationUrl = ({
|
||||
accountId,
|
||||
activeInbox,
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
conversationUrl,
|
||||
accountIdFromPathname,
|
||||
isValidURL,
|
||||
getLoginRedirectURL,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
|
@ -58,4 +59,24 @@ describe('#URL Helpers', () => {
|
|||
expect(isValidURL('alert.window')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLoginRedirectURL', () => {
|
||||
it('should return correct Account URL if account id is present', () => {
|
||||
expect(
|
||||
getLoginRedirectURL('7500', {
|
||||
accounts: [{ id: 7500, name: 'Test Account 7500' }],
|
||||
})
|
||||
).toBe('/app/accounts/7500/dashboard');
|
||||
});
|
||||
|
||||
it('should return default URL if account id is not present', () => {
|
||||
expect(getLoginRedirectURL('7500', {})).toBe('/app/');
|
||||
expect(
|
||||
getLoginRedirectURL('7500', {
|
||||
accounts: [{ id: '7501', name: 'Test Account 7501' }],
|
||||
})
|
||||
).toBe('/app/');
|
||||
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,10 @@
|
|||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than"
|
||||
},
|
||||
"ATTRIBUTE_LABELS": {
|
||||
"TRUE": "True",
|
||||
"FALSE": "False"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Status",
|
||||
"ASSIGNEE_NAME": "Assignee Name",
|
||||
|
|
|
@ -90,6 +90,18 @@
|
|||
},
|
||||
"ACTION": {
|
||||
"DELETE_MESSAGE": "You need to have atleast one action to save"
|
||||
},
|
||||
"TOGGLE": {
|
||||
"ACTIVATION_TITLE": "Activate Automation Rule",
|
||||
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
|
||||
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
|
||||
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
|
||||
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
|
||||
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
|
||||
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
|
||||
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
|
||||
"CONFIRMATION_LABEL": "Yes",
|
||||
"CANCEL_LABEL": "No"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,14 @@
|
|||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
"TIP_AUDIORECORDER_PERMISSION": "Allow access to audio",
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"ENTER_TO_SEND": "Enter to send",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"": "",
|
||||
"EMAIL_HEAD": {
|
||||
"ADD_BCC": "Add bcc",
|
||||
"CC": {
|
||||
|
|
|
@ -333,6 +333,11 @@
|
|||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
"FILTERS": {
|
||||
"AGENTS": {
|
||||
"PLACEHOLDER": "Choose Agents"
|
||||
}
|
||||
},
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"CONTACT_NAME": "Contact",
|
||||
|
|
|
@ -178,7 +178,8 @@
|
|||
"REPORTS_LABEL": "Labels",
|
||||
"REPORTS_INBOX": "Inbox",
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as"
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"BETA": "Beta"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
|
|
@ -147,6 +147,12 @@ export default {
|
|||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'text':
|
||||
return 'plain_text';
|
||||
case 'list':
|
||||
return 'search_select';
|
||||
case 'checkbox':
|
||||
return 'search_select';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
|
@ -164,6 +170,44 @@ export default {
|
|||
return type.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
](this.attributeModel);
|
||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type &&
|
||||
attr.attribute_display_type === 'checkbox'
|
||||
);
|
||||
});
|
||||
if (isCustomAttributeCheckbox) {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeList) {
|
||||
return allCustomAttributes
|
||||
.find(attr => attr.attribute_key === type)
|
||||
.attribute_values.map(item => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
switch (type) {
|
||||
case 'country_code':
|
||||
return countries;
|
||||
|
|
|
@ -253,7 +253,7 @@ export default {
|
|||
this.$store.dispatch('contacts/get', requestParams);
|
||||
} else {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: value,
|
||||
search: encodeURIComponent(value),
|
||||
...requestParams,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -130,11 +130,13 @@ export default {
|
|||
facebook: '',
|
||||
twitter: '',
|
||||
linkedin: '',
|
||||
github: '',
|
||||
},
|
||||
socialProfileKeys: [
|
||||
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
|
||||
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
|
||||
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
|
||||
{ key: 'github', prefixURL: 'https://github.com/' },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
@ -183,6 +185,7 @@ export default {
|
|||
twitter: socialProfiles.twitter || twitterScreenName || '',
|
||||
facebook: socialProfiles.facebook || '',
|
||||
linkedin: socialProfiles.linkedin || '',
|
||||
github: socialProfiles.github || '',
|
||||
};
|
||||
},
|
||||
getContactObject() {
|
||||
|
|
|
@ -34,12 +34,19 @@
|
|||
<td>{{ automation.name }}</td>
|
||||
<td>{{ automation.description }}</td>
|
||||
<td>
|
||||
<fluent-icon
|
||||
v-if="automation.active"
|
||||
icon="checkmark-square"
|
||||
type="solid"
|
||||
/>
|
||||
<fluent-icon v-else icon="square" />
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active: automation.active }"
|
||||
role="switch"
|
||||
:aria-checked="automation.active.toString()"
|
||||
@click="toggleAutomation(automation, automation.active)"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="{ active: automation.active }"
|
||||
></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ readableTime(automation.created_on) }}</td>
|
||||
<td class="button-wrapper">
|
||||
|
@ -120,6 +127,11 @@
|
|||
@saveAutomation="submitAutomation"
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-confirm-modal
|
||||
ref="confirmDialog"
|
||||
:title="toggleModalTitle"
|
||||
:description="toggleModalDescription"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -142,6 +154,10 @@ export default {
|
|||
showEditPopup: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedResponse: {},
|
||||
toggleModalTitle: this.$t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'),
|
||||
toggleModalDescription: this.$t(
|
||||
'AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION'
|
||||
),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -235,6 +251,34 @@ export default {
|
|||
this.showAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async toggleAutomation(automation, status) {
|
||||
try {
|
||||
this.toggleModalTitle = status
|
||||
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_TITLE')
|
||||
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_TITLE');
|
||||
this.toggleModalDescription = status
|
||||
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_DESCRIPTION', {
|
||||
automationName: automation.name,
|
||||
})
|
||||
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', {
|
||||
automationName: automation.name,
|
||||
});
|
||||
// Check if uses confirms to proceed
|
||||
const ok = await this.$refs.confirmDialog.showConfirmation();
|
||||
if (ok) {
|
||||
await await this.$store.dispatch('automations/update', {
|
||||
id: automation.id,
|
||||
active: !status,
|
||||
});
|
||||
const message = status
|
||||
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_SUCCESFUL')
|
||||
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_SUCCESFUL');
|
||||
this.showAlert(message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('AUTOMATION.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
readableTime(date) {
|
||||
return this.messageStamp(new Date(date), 'LLL d, h:mm a');
|
||||
},
|
||||
|
@ -246,4 +290,41 @@ export default {
|
|||
.automation__status-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
.toggle-button {
|
||||
background-color: var(--s-200);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: 19px;
|
||||
width: 34px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--border-radius-large);
|
||||
cursor: pointer;
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--w-500);
|
||||
}
|
||||
|
||||
.toggle-button span {
|
||||
--space-one-point-five: 1.5rem;
|
||||
height: var(--space-one-point-five);
|
||||
width: var(--space-one-point-five);
|
||||
display: inline-block;
|
||||
background-color: var(--white);
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||
transform: translate(0, 0);
|
||||
border-radius: 100%;
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
.toggle-button span.active {
|
||||
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<report-filter-selector @date-range-change="onDateRangeChange" />
|
||||
<report-filter-selector
|
||||
agents-filter
|
||||
:agents-filter-items-list="agentList"
|
||||
@date-range-change="onDateRangeChange"
|
||||
@agents-filter-change="onAgentsFilterChange"
|
||||
/>
|
||||
<csat-metrics />
|
||||
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
|
||||
</div>
|
||||
|
@ -9,6 +14,7 @@
|
|||
import CsatMetrics from './components/CsatMetrics';
|
||||
import CsatTable from './components/CsatTable';
|
||||
import ReportFilterSelector from './components/FilterSelector';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'CsatResponses',
|
||||
|
@ -18,11 +24,23 @@ export default {
|
|||
ReportFilterSelector,
|
||||
},
|
||||
data() {
|
||||
return { pageIndex: 1, from: 0, to: 0 };
|
||||
return { pageIndex: 1, from: 0, to: 0, user_ids: [] };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agentList: 'agents/getAgents',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
getAllData() {
|
||||
this.$store.dispatch('csat/getMetrics', { from: this.from, to: this.to });
|
||||
this.$store.dispatch('csat/getMetrics', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
user_ids: this.user_ids,
|
||||
});
|
||||
this.getResponses();
|
||||
},
|
||||
getResponses() {
|
||||
|
@ -30,6 +48,7 @@ export default {
|
|||
page: this.pageIndex,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
user_ids: this.user_ids,
|
||||
});
|
||||
},
|
||||
onPageNumberChange(pageIndex) {
|
||||
|
@ -41,6 +60,10 @@ export default {
|
|||
this.to = to;
|
||||
this.getAllData();
|
||||
},
|
||||
onAgentsFilterChange(agents) {
|
||||
this.user_ids = agents.map(el => el.id);
|
||||
this.getAllData();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
</woot-button>
|
||||
|
||||
<report-filter-selector
|
||||
group-by-filter
|
||||
:selected-group-by-filter="selectedGroupByFilter"
|
||||
:filter-items-list="filterItemsList"
|
||||
@date-range-change="onDateRangeChange"
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
@change="onChange"
|
||||
/>
|
||||
<div
|
||||
v-if="notLast7Days"
|
||||
v-if="notLast7Days && groupByFilter"
|
||||
class="small-12 medium-3 pull-right margin-left-small"
|
||||
>
|
||||
<p aria-hidden="true" class="hide">
|
||||
|
@ -41,6 +41,26 @@
|
|||
@input="changeFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="agentsFilter"
|
||||
class="small-12 medium-3 pull-right margin-left-small"
|
||||
>
|
||||
<multiselect
|
||||
v-model="selectedAgents"
|
||||
:options="agentsFilterItemsList"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:hide-selected="true"
|
||||
:placeholder="$t('CSAT_REPORTS.FILTERS.AGENTS.PLACEHOLDER')"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||
@input="handleAgentsFilterSelection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -61,10 +81,22 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
agentsFilterItemsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedGroupByFilter: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
groupByFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
agentsFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -72,6 +104,7 @@ export default {
|
|||
dateRange: this.$t('REPORT.DATE_RANGE'),
|
||||
customDateRange: [new Date(), new Date()],
|
||||
currentSelectedFilter: null,
|
||||
selectedAgents: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -149,6 +182,9 @@ export default {
|
|||
changeFilterSelection() {
|
||||
this.$emit('filter-change', this.currentSelectedFilter);
|
||||
},
|
||||
handleAgentsFilterSelection() {
|
||||
this.$emit('agents-filter-change', this.selectedAgents);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -5,6 +5,7 @@ import login from './login/login.routes';
|
|||
import dashboard from './dashboard/dashboard.routes';
|
||||
import authRoute from './auth/auth.routes';
|
||||
import { frontendURL } from '../helper/URLHelper';
|
||||
import { clearBrowserSessionCookies } from '../store/utils/api';
|
||||
|
||||
const routes = [
|
||||
...login.routes,
|
||||
|
@ -101,6 +102,13 @@ export const validateAuthenticateRoutePermission = (to, from, next) => {
|
|||
return nextRoute ? next(frontendURL(nextRoute)) : next();
|
||||
};
|
||||
|
||||
const validateSSOLoginParams = to => {
|
||||
const isLoginRoute = to.name === 'login';
|
||||
const { email, sso_auth_token: ssoAuthToken } = to.query || {};
|
||||
const hasValidSSOParams = email && ssoAuthToken;
|
||||
return isLoginRoute && hasValidSSOParams;
|
||||
};
|
||||
|
||||
const validateRouteAccess = (to, from, next) => {
|
||||
if (
|
||||
window.chatwootConfig.signupEnabled !== 'true' &&
|
||||
|
@ -111,6 +119,11 @@ const validateRouteAccess = (to, from, next) => {
|
|||
next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||
}
|
||||
|
||||
if (validateSSOLoginParams(to)) {
|
||||
clearBrowserSessionCookies();
|
||||
return next();
|
||||
}
|
||||
|
||||
if (authIgnoreRoutes.includes(to.name)) {
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ export default {
|
|||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
ssoAuthToken: { type: String, default: '' },
|
||||
ssoAccountId: { type: String, default: '' },
|
||||
redirectUrl: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
email: { type: String, default: '' },
|
||||
|
@ -138,6 +139,7 @@ export default {
|
|||
: this.credentials.email,
|
||||
password: this.credentials.password,
|
||||
sso_auth_token: this.ssoAuthToken,
|
||||
ssoAccountId: this.ssoAccountId,
|
||||
};
|
||||
this.$store
|
||||
.dispatch('login', credentials)
|
||||
|
|
|
@ -12,6 +12,7 @@ export default {
|
|||
email: route.query.email,
|
||||
ssoAuthToken: route.query.sso_auth_token,
|
||||
redirectUrl: route.query.route_url,
|
||||
ssoAccountId: route.query.sso_account_id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
|
|
@ -6,7 +6,7 @@ import authAPI from '../../api/auth';
|
|||
import createAxios from '../../helper/APIHelper';
|
||||
import actionCable from '../../helper/actionCable';
|
||||
import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api';
|
||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||
import { getLoginRedirectURL } from '../../helper/URLHelper';
|
||||
|
||||
const state = {
|
||||
currentUser: {
|
||||
|
@ -88,15 +88,16 @@ export const getters = {
|
|||
|
||||
// actions
|
||||
export const actions = {
|
||||
login({ commit }, credentials) {
|
||||
login({ commit }, { ssoAccountId, ...credentials }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
authAPI
|
||||
.login(credentials)
|
||||
.then(() => {
|
||||
.then(response => {
|
||||
commit(types.default.SET_CURRENT_USER);
|
||||
window.axios = createAxios(axios);
|
||||
actionCable.init(Vue);
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
|
||||
window.location = getLoginRedirectURL(ssoAccountId, response.data);
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
|
@ -82,10 +82,13 @@ export const getters = {
|
|||
};
|
||||
|
||||
export const actions = {
|
||||
get: async function getResponses({ commit }, { page = 1, from, to } = {}) {
|
||||
get: async function getResponses(
|
||||
{ commit },
|
||||
{ page = 1, from, to, user_ids } = {}
|
||||
) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await CSATReports.get({ page, from, to });
|
||||
const response = await CSATReports.get({ page, from, to, user_ids });
|
||||
commit(types.SET_CSAT_RESPONSE, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
@ -93,10 +96,10 @@ export const actions = {
|
|||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
getMetrics: async function getMetrics({ commit }, { from, to }) {
|
||||
getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) {
|
||||
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
|
||||
try {
|
||||
const response = await CSATReports.getMetrics({ from, to });
|
||||
const response = await CSATReports.getMetrics({ from, to, user_ids });
|
||||
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
|
|
@ -38,13 +38,15 @@ export const setAuthCredentials = response => {
|
|||
setUser(response.data.data, expiryDate);
|
||||
};
|
||||
|
||||
export const clearBrowserSessionCookies = () => {
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
};
|
||||
|
||||
export const clearCookiesOnLogout = () => {
|
||||
window.bus.$emit(CHATWOOT_RESET);
|
||||
window.bus.$emit(ANALYTICS_RESET);
|
||||
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
|
||||
clearBrowserSessionCookies();
|
||||
const globalConfig = window.globalConfig || {};
|
||||
const logoutRedirectLink =
|
||||
globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login');
|
||||
|
|
|
@ -152,7 +152,7 @@ export const IFrameHelper = {
|
|||
if (window.$chatwoot.user) {
|
||||
IFrameHelper.sendMessage('set-user', window.$chatwoot.user);
|
||||
}
|
||||
|
||||
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_READY });
|
||||
|
||||
window.playAudioAlert = () => {};
|
||||
|
|
|
@ -86,6 +86,11 @@
|
|||
"merge-outline": "M3 6.75A.75.75 0 0 1 3.75 6h4.5a.75.75 0 0 1 .53.22L13.56 11h5.878L15.72 7.28a.75.75 0 1 1 1.06-1.06l4.998 5a.75.75 0 0 1 0 1.06l-4.998 5a.75.75 0 1 1-1.06-1.06l3.718-3.72H13.56l-4.78 4.78a.75.75 0 0 1-.531.22h-4.5a.75.75 0 0 1 0-1.5h4.19l4.25-4.25L7.94 7.5H3.75A.75.75 0 0 1 3 6.75Z",
|
||||
"more-horizontal-outline": "M7.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM13.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM18 13.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z",
|
||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||
"microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z",
|
||||
"microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z",
|
||||
"microphone-stop-outline": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9",
|
||||
"microphone-pause-outline": "M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
|
||||
"microphone-play-outline": "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z",
|
||||
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
|
||||
|
@ -120,6 +125,7 @@
|
|||
"brand-telegram-outline": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
|
||||
"brand-twitter-outline": "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z",
|
||||
"brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z",
|
||||
"brand-github-outline": "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z",
|
||||
"add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z",
|
||||
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z"
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export const fileSizeInMegaBytes = bytes => {
|
|||
};
|
||||
|
||||
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
||||
const fileSize = file?.file?.size;
|
||||
const fileSize = file?.file?.size || file?.size;
|
||||
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
|
||||
return fileSizeInMB <= maximumUploadLimit;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import marked from 'marked';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { escapeHtml } from './HTMLSanitizer';
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@ export default {
|
|||
const { medium: medium = '' } = this.inbox;
|
||||
return this.isATwilioChannel && medium === 'sms';
|
||||
},
|
||||
isASmsInbox() {
|
||||
return this.channelType === INBOX_TYPES.SMS || this.isATwilioSMSChannel;
|
||||
},
|
||||
isATwilioWhatsappChannel() {
|
||||
const { medium: medium = '' } = this.inbox;
|
||||
return this.isATwilioChannel && medium === 'whatsapp';
|
||||
|
|
|
@ -62,6 +62,18 @@ describe('inboxMixin', () => {
|
|||
expect(wrapper.vm.isAWebWidgetInbox).toBe(true);
|
||||
});
|
||||
|
||||
it('isASmsInbox returns true if channel type is sms', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return { inbox: { channel_type: 'Channel::Sms' } };
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isASmsInbox).toBe(true);
|
||||
});
|
||||
|
||||
it('isATwilioChannel returns true if channel type is Twilio', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
|
@ -94,6 +106,7 @@ describe('inboxMixin', () => {
|
|||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isATwilioChannel).toBe(true);
|
||||
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
|
||||
expect(wrapper.vm.isASmsInbox).toBe(true);
|
||||
});
|
||||
|
||||
it('isATwilioWhatsappChannel returns true if channel type is Twilio and medium is whatsapp', () => {
|
||||
|
|
|
@ -37,12 +37,13 @@ import CustomButton from 'shared/components/Button';
|
|||
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { sendEmailTranscript } from 'widget/api/conversation';
|
||||
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
export default {
|
||||
components: {
|
||||
ChatInputWrap,
|
||||
CustomButton,
|
||||
},
|
||||
mixins: [routerMixin],
|
||||
props: {
|
||||
msg: {
|
||||
type: String,
|
||||
|
@ -53,7 +54,7 @@ export default {
|
|||
...mapGetters({
|
||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
getConversationSize: 'conversation/getConversationSize',
|
||||
conversationSize: 'conversation/getConversationSize',
|
||||
currentUser: 'contacts/getCurrentUser',
|
||||
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
|
||||
}),
|
||||
|
@ -80,12 +81,11 @@ export default {
|
|||
'clearConversationAttributes',
|
||||
]),
|
||||
async handleSendMessage(content) {
|
||||
const conversationSize = this.getConversationSize;
|
||||
await this.sendMessage({
|
||||
content,
|
||||
});
|
||||
// Update conversation attributes on new conversation
|
||||
if (conversationSize === 0) {
|
||||
if (this.conversationSize === 0) {
|
||||
this.getAttributes();
|
||||
}
|
||||
},
|
||||
|
@ -95,7 +95,12 @@ export default {
|
|||
startNewConversation() {
|
||||
this.clearConversations();
|
||||
this.clearConversationAttributes();
|
||||
window.bus.$emit(BUS_EVENTS.START_NEW_CONVERSATION);
|
||||
|
||||
// To create a new conversation, we are redirecting
|
||||
// the user to pre-chat with contact fields disabled
|
||||
// Pass disableContactFields params to the route
|
||||
// This would disable the contact fields in the pre-chat form
|
||||
this.replaceRoute('prechat-form', { disableContactFields: true });
|
||||
},
|
||||
async sendTranscript() {
|
||||
const { email } = this.currentUser;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{{ headerMessage }}
|
||||
</div>
|
||||
<form-input
|
||||
v-if="options.requireEmail"
|
||||
v-if="areContactFieldsVisible"
|
||||
v-model="fullName"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')"
|
||||
|
@ -21,7 +21,7 @@
|
|||
"
|
||||
/>
|
||||
<form-input
|
||||
v-if="options.requireEmail"
|
||||
v-if="areContactFieldsVisible"
|
||||
v-model="emailAddress"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')"
|
||||
|
@ -77,6 +77,10 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
disableContactFields: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
validations() {
|
||||
const identityValidations = {
|
||||
|
@ -99,7 +103,7 @@ export default {
|
|||
if (this.hasActiveCampaign) {
|
||||
return identityValidations;
|
||||
}
|
||||
if (this.options.requireEmail) {
|
||||
if (this.areContactFieldsVisible) {
|
||||
return {
|
||||
...identityValidations,
|
||||
...messageValidation,
|
||||
|
@ -135,6 +139,9 @@ export default {
|
|||
}
|
||||
return this.options.preChatMessage;
|
||||
},
|
||||
areContactFieldsVisible() {
|
||||
return this.options.requireEmail && !this.disableContactFields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export default {
|
||||
methods: {
|
||||
async replaceRoute(name) {
|
||||
async replaceRoute(name, params = {}) {
|
||||
if (this.$route.name !== name) {
|
||||
return this.$router.replace({ name });
|
||||
return this.$router.replace({ name, params });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
import configMixin from '../mixins/configMixin';
|
||||
import TeamAvailability from 'widget/components/TeamAvailability';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
export default {
|
||||
name: 'Home',
|
||||
|
@ -34,10 +33,7 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOnCollapsedView: false,
|
||||
isOnNewConversation: false,
|
||||
};
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -46,12 +42,6 @@ export default {
|
|||
conversationSize: 'conversation/getConversationSize',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
|
||||
this.isOnCollapsedView = true;
|
||||
this.isOnNewConversation = true;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
if (this.preChatFormEnabled && !this.conversationSize) {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<pre-chat-form :options="preChatFormOptions" @submit="onSubmit" />
|
||||
<pre-chat-form
|
||||
:options="preChatFormOptions"
|
||||
:disable-contact-fields="disableContactFields"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -18,6 +22,10 @@ export default {
|
|||
...mapGetters({
|
||||
conversationSize: 'conversation/getConversationSize',
|
||||
}),
|
||||
disableContactFields() {
|
||||
const { disableContactFields = false } = this.$route.params || {};
|
||||
return disableContactFields;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversationSize(newSize, oldSize) {
|
||||
|
|
|
@ -45,7 +45,8 @@ class AutomationRuleListener < BaseListener
|
|||
|
||||
@rules = AutomationRule.where(
|
||||
event_name: event_name,
|
||||
account_id: conversation.account_id
|
||||
account_id: conversation.account_id,
|
||||
active: true
|
||||
)
|
||||
@rules.any?
|
||||
end
|
||||
|
|
|
@ -26,7 +26,7 @@ class AgentBot < ApplicationRecord
|
|||
has_many :agent_bot_inboxes, dependent: :destroy_async
|
||||
has_many :inboxes, through: :agent_bot_inboxes
|
||||
has_many :messages, as: :sender, dependent: :restrict_with_exception
|
||||
belongs_to :account, dependent: :destroy_async, optional: true
|
||||
belongs_to :account, optional: true
|
||||
|
||||
def available_name
|
||||
name
|
||||
|
|
|
@ -48,6 +48,10 @@ class Attachment < ApplicationRecord
|
|||
file.attached? ? url_for(file) : ''
|
||||
end
|
||||
|
||||
def download_url
|
||||
file.attached? ? rails_storage_proxy_url(file) : ''
|
||||
end
|
||||
|
||||
def thumb_url
|
||||
if file.attached? && file.representable?
|
||||
url_for(file.representation(resize: '250x250'))
|
||||
|
|
|
@ -33,35 +33,38 @@ class Channel::Sms < ApplicationRecord
|
|||
'https://messaging.bandwidth.com/api/v2'
|
||||
end
|
||||
|
||||
# Extract later into provider Service
|
||||
def send_message(phone_number, message)
|
||||
if message.attachments.present?
|
||||
send_attachment_message(phone_number, message)
|
||||
else
|
||||
send_text_message(phone_number, message.content)
|
||||
end
|
||||
def send_message(contact_number, message)
|
||||
body = message_body(contact_number, message.content)
|
||||
body['media'] = message.attachments.map(&:download_url) if message.attachments.present?
|
||||
|
||||
send_to_bandwidth(body)
|
||||
end
|
||||
|
||||
def send_text_message(contact_number, message)
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/users/#{provider_config['account_id']}/messages",
|
||||
basic_auth: bandwidth_auth,
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: {
|
||||
'to' => contact_number,
|
||||
'from' => phone_number,
|
||||
'text' => message,
|
||||
'applicationId' => provider_config['application_id']
|
||||
}.to_json
|
||||
)
|
||||
|
||||
response.success? ? response.parsed_response['id'] : nil
|
||||
def send_text_message(contact_number, message_content)
|
||||
body = message_body(contact_number, message_content)
|
||||
send_to_bandwidth(body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_attachment_message(phone_number, message)
|
||||
# fix me
|
||||
def message_body(contact_number, message_content)
|
||||
{
|
||||
'to' => contact_number,
|
||||
'from' => phone_number,
|
||||
'text' => message_content,
|
||||
'applicationId' => provider_config['application_id']
|
||||
}
|
||||
end
|
||||
|
||||
def send_to_bandwidth(body)
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/users/#{provider_config['account_id']}/messages",
|
||||
basic_auth: bandwidth_auth,
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: body.to_json
|
||||
)
|
||||
|
||||
response.success? ? response.parsed_response['id'] : nil
|
||||
end
|
||||
|
||||
def bandwidth_auth
|
||||
|
|
|
@ -40,4 +40,7 @@ class CsatSurveyResponse < ApplicationRecord
|
|||
validates :account_id, presence: true
|
||||
validates :contact_id, presence: true
|
||||
validates :conversation_id, presence: true
|
||||
|
||||
scope :filter_by_created_at, ->(range) { where(created_at: range) if range.present? }
|
||||
scope :filter_by_assigned_agent_id, ->(user_ids) { where(assigned_agent_id: user_ids) if user_ids.present? }
|
||||
end
|
||||
|
|
|
@ -14,6 +14,8 @@ class Sms::IncomingMessageService
|
|||
sender: @contact,
|
||||
source_id: params[:id]
|
||||
)
|
||||
attach_files
|
||||
@message.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -22,6 +24,10 @@ class Sms::IncomingMessageService
|
|||
@account ||= @inbox.account
|
||||
end
|
||||
|
||||
def channel
|
||||
@channel ||= @inbox.channel
|
||||
end
|
||||
|
||||
def phone_number
|
||||
params[:from]
|
||||
end
|
||||
|
@ -63,4 +69,28 @@ class Sms::IncomingMessageService
|
|||
phone_number: phone_number
|
||||
}
|
||||
end
|
||||
|
||||
def attach_files
|
||||
return if params[:media].blank?
|
||||
|
||||
params[:media].each do |media_url|
|
||||
# we don't need to process this files since chatwoot doesn't support it
|
||||
next if media_url.end_with? '.smil'
|
||||
|
||||
attachment_file = Down.download(
|
||||
media_url,
|
||||
http_basic_authentication: [channel.provider_config['api_key'], channel.provider_config['api_secret']]
|
||||
)
|
||||
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(attachment_file.content_type),
|
||||
file: {
|
||||
io: attachment_file,
|
||||
filename: attachment_file,
|
||||
content_type: attachment_file.content_type
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<% account_user = @resource&.account_users&.first %>
|
||||
<% if account_user&.inviter.present? && @resource.unconfirmed_email.blank? %>
|
||||
<p><%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out Chatwoot! </p>
|
||||
<p><%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out <%= global_config['BRAND_NAME'] || 'Chatwoot' %>! </p>
|
||||
<% end %>
|
||||
|
||||
<p>You can confirm your account email through the link below:</p>
|
||||
|
@ -11,4 +11,4 @@
|
|||
<p><%= link_to 'Confirm my account', frontend_url('auth/password/edit', reset_password_token: @resource.send(:set_reset_password_token)) %></p>
|
||||
<% else %>
|
||||
<p><%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"ionicons": "~2.0.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"marked": "2.0.3",
|
||||
"marked": "4.0.10",
|
||||
"md5": "^2.3.0",
|
||||
"ninja-keys": "^1.1.9",
|
||||
"posthog-js": "^1.13.7",
|
||||
|
@ -50,6 +50,7 @@
|
|||
"prosemirror-state": "1.3.4",
|
||||
"prosemirror-view": "1.18.4",
|
||||
"query-string": "5",
|
||||
"recordrtc": "^5.6.2",
|
||||
"semver": "7.3.5",
|
||||
"spinkit": "~1.2.5",
|
||||
"tailwindcss": "^1.9.6",
|
||||
|
@ -71,7 +72,8 @@
|
|||
"vuedraggable": "^2.24.3",
|
||||
"vuelidate": "0.7.6",
|
||||
"vuex": "~2.1.1",
|
||||
"vuex-router-sync": "~4.1.2"
|
||||
"vuex-router-sync": "~4.1.2",
|
||||
"wavesurfer.js": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.13.16",
|
||||
|
|
|
@ -48,6 +48,25 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
|||
expect(response_data.pluck('id')).not_to include(csat_10_days_ago.id)
|
||||
end
|
||||
|
||||
it 'filters csat responses based on a date range and agent ids' do
|
||||
csat1_assigned_agent = create(:user, account: account, role: :agent)
|
||||
csat2_assigned_agent = create(:user, account: account, role: :agent)
|
||||
|
||||
create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent)
|
||||
create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent)
|
||||
create(:csat_survey_response, account: account, created_at: 5.days.ago)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/csat_survey_responses",
|
||||
params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s,
|
||||
user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
expect(response_data.size).to eq 2
|
||||
end
|
||||
|
||||
it 'returns csat responses even if the agent is deleted from account' do
|
||||
deleted_agent_csat = create(:csat_survey_response, account: account, assigned_agent: agent)
|
||||
deleted_agent_csat.assigned_agent.account_users.destroy_all
|
||||
|
@ -106,6 +125,27 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
|
|||
expect(response_data['total_sent_messages_count']).to eq 0
|
||||
expect(response_data['ratings_count']).to eq({ '1' => 1 })
|
||||
end
|
||||
|
||||
it 'filters csat metrics based on a date range and agent ids' do
|
||||
csat1_assigned_agent = create(:user, account: account, role: :agent)
|
||||
csat2_assigned_agent = create(:user, account: account, role: :agent)
|
||||
|
||||
create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent)
|
||||
create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent)
|
||||
create(:csat_survey_response, account: account, created_at: 5.days.ago)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics",
|
||||
params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s,
|
||||
user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body)
|
||||
expect(response_data['total_count']).to eq 2
|
||||
expect(response_data['total_sent_messages_count']).to eq 0
|
||||
expect(response_data['ratings_count']).to eq({ '1' => 2 })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,37 @@ describe Sms::IncomingMessageService do
|
|||
expect(Contact.all.first.name).to eq('+1 423-423-4234')
|
||||
expect(sms_channel.inbox.messages.first.content).to eq('test message')
|
||||
end
|
||||
|
||||
it 'creates attachment messages and ignores .smil files' do
|
||||
stub_request(:get, 'http://test.com/test.png').to_return(status: 200, body: File.read('spec/assets/sample.png'), headers: {})
|
||||
stub_request(:get, 'http://test.com/test2.png').to_return(status: 200, body: File.read('spec/assets/sample.png'), headers: {})
|
||||
|
||||
params = {
|
||||
|
||||
'id': '3232420-2323-234324',
|
||||
'owner': sms_channel.phone_number,
|
||||
'applicationId': '2342349-324234d-32432432',
|
||||
'time': '2022-02-02T23:14:05.262Z',
|
||||
'segmentCount': 1,
|
||||
'direction': 'in',
|
||||
'to': [
|
||||
sms_channel.phone_number
|
||||
],
|
||||
'media': [
|
||||
'http://test.com/test.smil',
|
||||
'http://test.com/test.png',
|
||||
'http://test.com/test2.png'
|
||||
],
|
||||
'from': '+14234234234',
|
||||
'text': 'test message'
|
||||
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: sms_channel.inbox, params: params).perform
|
||||
expect(sms_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('+1 423-423-4234')
|
||||
expect(sms_channel.inbox.messages.first.content).to eq('test message')
|
||||
expect(sms_channel.inbox.messages.first.attachments.present?).to eq true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,29 @@ describe Sms::SendOnSmsService do
|
|||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'calls channel.send_message with attachments' do
|
||||
message = build(:message, message_type: :outgoing, content: 'test',
|
||||
conversation: conversation)
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
attachment2 = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
attachment2.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
message.save!
|
||||
|
||||
allow(HTTParty).to receive(:post).and_return(sms_request)
|
||||
allow(sms_request).to receive(:success?).and_return(true)
|
||||
allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' })
|
||||
expect(HTTParty).to receive(:post).with(
|
||||
'https://messaging.bandwidth.com/api/v2/users/1/messages',
|
||||
basic_auth: { username: '1', password: '1' },
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1',
|
||||
'media' => [attachment.download_url, attachment2.download_url] }.to_json
|
||||
)
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -9840,10 +9840,10 @@ markdown-to-jsx@^7.1.0:
|
|||
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.2.tgz#19d3da4cd8864045cdd13a0d179147fbd6a088d4"
|
||||
integrity sha512-O8DMCl32V34RrD+ZHxcAPc2+kYytuDIoQYjY36RVdsLK7uHjgNVvFec4yv0X6LgB4YEZgSvK5QtFi5YVqEpoMA==
|
||||
|
||||
marked@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
|
||||
integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
|
||||
marked@4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.10.tgz#423e295385cc0c3a70fa495e0df68b007b879423"
|
||||
integrity sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==
|
||||
|
||||
material-colors@^1.0.0:
|
||||
version "1.2.6"
|
||||
|
@ -12747,6 +12747,11 @@ recast@^0.18.1:
|
|||
private "^0.1.8"
|
||||
source-map "~0.6.1"
|
||||
|
||||
recordrtc@^5.6.2:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/recordrtc/-/recordrtc-5.6.2.tgz#48fc214b35084973ccce82c6251198b5742bc327"
|
||||
integrity sha512-1QNKKNtl7+KcwD1lyOgP3ZlbiJ1d0HtXnypUy7yq49xEERxk31PHvE9RCciDrulPCY7WJ+oz0R9hpNxgsIurGQ==
|
||||
|
||||
recursive-readdir@2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
|
||||
|
@ -14850,9 +14855,9 @@ url-loader@^4.1.1:
|
|||
schema-utils "^3.0.0"
|
||||
|
||||
url-parse@^1.4.3, url-parse@^1.5.1:
|
||||
version "1.5.7"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
|
||||
integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
@ -15286,6 +15291,11 @@ watchpack@^1.7.4:
|
|||
chokidar "^3.4.1"
|
||||
watchpack-chokidar2 "^2.0.1"
|
||||
|
||||
wavesurfer.js@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz#efae107b5b561e9bfe3fffc50e6158136a17643e"
|
||||
integrity sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g==
|
||||
|
||||
wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
|
||||
|
|
Loading…
Reference in a new issue