Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e919256e6 | ||
|
|
8ae7c15301 | ||
|
|
325dc4a741 | ||
|
|
a8001ccabc | ||
|
|
557df5c5c9 | ||
|
|
cc4924db55 | ||
|
|
ae694da6c1 | ||
|
|
2024b9e90d | ||
|
|
7e54b13a8b | ||
|
|
29158e32fe | ||
|
|
d0631e99a5 | ||
|
|
ba44f4fff8 | ||
|
|
00556bd55a | ||
|
|
15353ee779 | ||
|
|
5c05e45206 | ||
|
|
86b948ecbd | ||
|
|
2dd9f0f672 | ||
|
|
dedd67167a | ||
|
|
d96ac59cca |
+1
-1
@@ -561,7 +561,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.12)
|
||||
rack (2.2.13)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
|
||||
@@ -2,10 +2,9 @@ class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
# TODO: should we only do ensure stream if current account is present?
|
||||
# for now going ahead with guard clauses in update_subscription and broadcast_presence
|
||||
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
prepare_stream
|
||||
update_subscription
|
||||
broadcast_presence
|
||||
end
|
||||
@@ -22,12 +21,21 @@ class RoomChannel < ApplicationCable::Channel
|
||||
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
ActionCable.server.broadcast(pubsub_token, { event: 'presence.update', data: data })
|
||||
end
|
||||
|
||||
def ensure_stream
|
||||
@pubsub_token = params[:pubsub_token]
|
||||
stream_from @pubsub_token
|
||||
def prepare_stream
|
||||
stream_from pubsub_token
|
||||
stream_from "account_#{@current_account.id}" if @current_account.present? && @current_user.is_a?(User)
|
||||
stream_from_inboxes
|
||||
end
|
||||
|
||||
def stream_from_inboxes
|
||||
return if current_user.blank?
|
||||
|
||||
current_user.assigned_inboxes.each do |inbox|
|
||||
stream_from "inbox_#{inbox.id}"
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
@@ -36,11 +44,15 @@ class RoomChannel < ApplicationCable::Channel
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
def pubsub_token
|
||||
@pubsub_token ||= params[:pubsub_token]
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= if params[:user_id].blank?
|
||||
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
|
||||
ContactInbox.find_by!(pubsub_token: pubsub_token).contact
|
||||
else
|
||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||
User.find_by!(pubsub_token: pubsub_token, id: params[:user_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
include MetaTokenVerifyConcern
|
||||
|
||||
def process_payload
|
||||
if inactive_whatsapp_number?
|
||||
Rails.logger.warn("Rejected webhook for inactive WhatsApp number: #{params[:phone_number]}")
|
||||
render json: { error: 'Inactive WhatsApp number' }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
@@ -13,4 +19,15 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present?
|
||||
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
|
||||
end
|
||||
|
||||
def inactive_whatsapp_number?
|
||||
phone_number = params[:phone_number]
|
||||
return false if phone_number.blank?
|
||||
|
||||
inactive_numbers = GlobalConfig.get_value('INACTIVE_WHATSAPP_NUMBERS').to_s
|
||||
return false if inactive_numbers.blank?
|
||||
|
||||
inactive_numbers_array = inactive_numbers.split(',').map(&:strip)
|
||||
inactive_numbers_array.include?(phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,7 +82,7 @@ input[type='url']:not(.reset-base) {
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
@apply bg-white dark:bg-n-solid-1 leading-[1.15] mb-4;
|
||||
@apply bg-n-background leading-[1.15] mb-4;
|
||||
}
|
||||
|
||||
// Select
|
||||
|
||||
@@ -93,7 +93,7 @@ const hasQuotedMessage = computed(() => {
|
||||
<template v-else>
|
||||
<Letter
|
||||
v-if="showQuotedMessage"
|
||||
class-name="prose prose-bubble !max-w-none"
|
||||
class-name="prose prose-bubble !max-w-none letter-render"
|
||||
:allowed-css-properties="[
|
||||
...allowedCssProperties,
|
||||
'transform',
|
||||
@@ -104,7 +104,7 @@ const hasQuotedMessage = computed(() => {
|
||||
/>
|
||||
<Letter
|
||||
v-else
|
||||
class-name="prose prose-bubble !max-w-none"
|
||||
class-name="prose prose-bubble !max-w-none letter-render"
|
||||
:html="unquotedHTML"
|
||||
:allowed-css-properties="[
|
||||
...allowedCssProperties,
|
||||
@@ -143,3 +143,21 @@ const hasQuotedMessage = computed(() => {
|
||||
</section>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// Tailwind resets break the rendering of google drive link in Gmail messages
|
||||
// This fixes it using https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
|
||||
|
||||
.letter-render [class*='gmail_drive_chip'] {
|
||||
box-sizing: initial;
|
||||
@apply bg-n-slate-4 border-n-slate-6 rounded-md !important;
|
||||
|
||||
a {
|
||||
@apply text-n-slate-12 !important;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import FormattedContent from './FormattedContent.vue';
|
||||
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||
@@ -9,6 +9,26 @@ import { useMessageContext } from '../../provider.js';
|
||||
const { content, attachments, contentAttributes, messageType } =
|
||||
useMessageContext();
|
||||
|
||||
const hasTranslations = computed(() => {
|
||||
const { translations = {} } = contentAttributes.value;
|
||||
return Object.keys(translations || {}).length > 0;
|
||||
});
|
||||
|
||||
const renderOriginal = ref(false);
|
||||
|
||||
const renderContent = computed(() => {
|
||||
if (renderOriginal.value) {
|
||||
return content.value;
|
||||
}
|
||||
|
||||
if (hasTranslations.value) {
|
||||
const translations = contentAttributes.value.translations;
|
||||
return translations[Object.keys(translations)[0]];
|
||||
}
|
||||
|
||||
return content.value;
|
||||
});
|
||||
|
||||
const isTemplate = computed(() => {
|
||||
return messageType.value === MESSAGE_TYPES.TEMPLATE;
|
||||
});
|
||||
@@ -16,6 +36,16 @@ const isTemplate = computed(() => {
|
||||
const isEmpty = computed(() => {
|
||||
return !content.value && !attachments.value?.length;
|
||||
});
|
||||
|
||||
const viewToggleKey = computed(() => {
|
||||
return renderOriginal.value
|
||||
? 'CONVERSATION.VIEW_TRANSLATED'
|
||||
: 'CONVERSATION.VIEW_ORIGINAL';
|
||||
});
|
||||
|
||||
const handleSeeOriginal = () => {
|
||||
renderOriginal.value = !renderOriginal.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +54,16 @@ const isEmpty = computed(() => {
|
||||
<span v-if="isEmpty" class="text-n-slate-11">
|
||||
{{ $t('CONVERSATION.NO_CONTENT') }}
|
||||
</span>
|
||||
<FormattedContent v-if="content" :content="content" />
|
||||
<FormattedContent v-if="renderContent" :content="renderContent" />
|
||||
<span class="-mt-3">
|
||||
<span
|
||||
v-if="hasTranslations"
|
||||
class="text-xs text-n-slate-11 cursor-pointer hover:underline"
|
||||
@click="handleSeeOriginal"
|
||||
>
|
||||
{{ $t(viewToggleKey) }}
|
||||
</span>
|
||||
</span>
|
||||
<AttachmentChips :attachments="attachments" class="gap-2" />
|
||||
<template v-if="isTemplate">
|
||||
<div
|
||||
|
||||
@@ -90,7 +90,7 @@ const sortedInboxes = computed(() =>
|
||||
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
const newReportRoutes = [
|
||||
const newReportRoutes = () => [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
label: t('SIDEBAR.REPORTS_AGENT'),
|
||||
@@ -116,7 +116,7 @@ const newReportRoutes = [
|
||||
},
|
||||
];
|
||||
|
||||
const oldReportRoutes = [
|
||||
const oldReportRoutes = () => [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
label: t('SIDEBAR.REPORTS_AGENT'),
|
||||
@@ -140,7 +140,7 @@ const oldReportRoutes = [
|
||||
];
|
||||
|
||||
const reportRoutes = computed(() =>
|
||||
showV4Routes.value ? newReportRoutes : oldReportRoutes
|
||||
showV4Routes.value ? newReportRoutes() : oldReportRoutes()
|
||||
);
|
||||
|
||||
const menuItems = computed(() => {
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-10 absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import router from '../../routes/index';
|
||||
const props = defineProps({
|
||||
backUrl: {
|
||||
@@ -24,24 +23,16 @@ const goBack = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const buttonStyleClass = props.compact
|
||||
? 'text-sm text-n-slate-11'
|
||||
: 'text-base text-n-blue-text';
|
||||
const buttonStyleClass = props.compact ? 'text-sm' : 'text-base';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center p-0 font-normal cursor-pointer gap-1"
|
||||
class="flex items-center p-0 font-normal cursor-pointer text-n-slate-11"
|
||||
:class="buttonStyleClass"
|
||||
@click.capture="goBack"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-left"
|
||||
class="ltr:-ml-1 rtl:-mr-1"
|
||||
:class="
|
||||
props.compact ? 'text-n-slate-11 size-4' : 'text-n-blue-text size-5'
|
||||
"
|
||||
/>
|
||||
<fluent-icon icon="chevron-left" class="-ml-1" />
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
contentAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
computed: {
|
||||
translationsAvailable() {
|
||||
return !!Object.keys(this.translations).length;
|
||||
},
|
||||
translations() {
|
||||
return this.contentAttributes.translations || {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal
|
||||
modal-type="right-aligned"
|
||||
class="text-left"
|
||||
show
|
||||
:on-close="onClose"
|
||||
>
|
||||
<div class="content">
|
||||
<p>
|
||||
<b>{{ $t('TRANSLATE_MODAL.ORIGINAL_CONTENT') }}</b>
|
||||
</p>
|
||||
<p v-dompurify-html="content" class="mb-0" />
|
||||
<br />
|
||||
<hr />
|
||||
<div v-if="translationsAvailable">
|
||||
<p>
|
||||
<b>{{ $t('TRANSLATE_MODAL.TRANSLATED_CONTENT') }}</b>
|
||||
</p>
|
||||
<div v-for="(translation, language) in translations" :key="language">
|
||||
<p>
|
||||
<strong>{{ language }}:</strong>
|
||||
</p>
|
||||
<p v-dompurify-html="translation" />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('TRANSLATE_MODAL.NO_TRANSLATIONS_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@@ -5,9 +5,26 @@ import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { allAgentsData, formattedAgentsData } from './fixtures/agentFixtures';
|
||||
import * as agentHelper from 'dashboard/helper/agentHelper';
|
||||
|
||||
// Mock vue-i18n
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: key => (key === 'AGENT_MGMT.MULTI_SELECTOR.LIST.NONE' ? 'None' : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/agentHelper');
|
||||
|
||||
// Create a mock None agent
|
||||
const mockNoneAgent = {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
|
||||
const mockUseMapGetter = (overrides = {}) => {
|
||||
const defaultGetters = {
|
||||
getCurrentUser: ref(allAgentsData[0]),
|
||||
@@ -28,14 +45,6 @@ describe('useAgentsList', () => {
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue(
|
||||
formattedAgentsData.slice(1)
|
||||
);
|
||||
agentHelper.getCombinedAgents.mockImplementation(
|
||||
(agents, includeNone, isAgentSelected) => {
|
||||
if (includeNone && isAgentSelected) {
|
||||
return [agentHelper.createNoneAgent, ...agents];
|
||||
}
|
||||
return agents;
|
||||
}
|
||||
);
|
||||
|
||||
mockUseMapGetter();
|
||||
});
|
||||
@@ -44,24 +53,26 @@ describe('useAgentsList', () => {
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual(allAgentsData);
|
||||
expect(agentsList.value).toEqual([
|
||||
agentHelper.createNoneAgent,
|
||||
...formattedAgentsData.slice(1),
|
||||
]);
|
||||
expect(agentsList.value[0]).toEqual(mockNoneAgent);
|
||||
expect(agentsList.value.length).toBe(
|
||||
formattedAgentsData.slice(1).length + 1
|
||||
);
|
||||
});
|
||||
|
||||
it('includes None agent when includeNoneAgent is true', () => {
|
||||
const { agentsList } = useAgentsList(true);
|
||||
|
||||
expect(agentsList.value[0]).toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length);
|
||||
expect(agentsList.value[0]).toEqual(mockNoneAgent);
|
||||
expect(agentsList.value.length).toBe(
|
||||
formattedAgentsData.slice(1).length + 1
|
||||
);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const { agentsList } = useAgentsList(false);
|
||||
|
||||
expect(agentsList.value[0]).not.toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length - 1);
|
||||
expect(agentsList.value[0].id).not.toBe(0);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.slice(1).length);
|
||||
});
|
||||
|
||||
it('handles empty assignable agents', () => {
|
||||
@@ -73,7 +84,7 @@ describe('useAgentsList', () => {
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
expect(agentsList.value).toEqual([mockNoneAgent]);
|
||||
});
|
||||
|
||||
it('handles missing inbox_id', () => {
|
||||
@@ -86,6 +97,6 @@ describe('useAgentsList', () => {
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
expect(agentsList.value).toEqual([mockNoneAgent]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
getAgentsByUpdatedPresence,
|
||||
getSortedAgentsByAvailability,
|
||||
getCombinedAgents,
|
||||
} from 'dashboard/helper/agentHelper';
|
||||
|
||||
/**
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
* @returns {Object} An object containing the agents list and assignable agents.
|
||||
*/
|
||||
export function useAgentsList(includeNoneAgent = true) {
|
||||
const { t } = useI18n();
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
@@ -21,6 +22,19 @@ export function useAgentsList(includeNoneAgent = true) {
|
||||
const inboxId = computed(() => currentChat.value?.inbox_id);
|
||||
const isAgentSelected = computed(() => currentChat.value?.meta?.assignee);
|
||||
|
||||
/**
|
||||
* Creates a 'None' agent object
|
||||
* @returns {Object} None agent object
|
||||
*/
|
||||
const createNoneAgent = () => ({
|
||||
confirmed: true,
|
||||
name: t('AGENT_MGMT.MULTI_SELECTOR.LIST.NONE') || 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
@@ -43,11 +57,10 @@ export function useAgentsList(includeNoneAgent = true) {
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
|
||||
return getCombinedAgents(
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected.value
|
||||
);
|
||||
return [
|
||||
...(includeNoneAgent && isAgentSelected.value ? [createNoneAgent()] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
export default function useAutomationValues() {
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agents = useMapGetter('agents/getAgents');
|
||||
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
||||
const contacts = useMapGetter('contacts/getContacts');
|
||||
@@ -61,6 +60,19 @@ export default function useAutomationValues() {
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds a translated "None" option to the beginning of a list
|
||||
* @param {Array} list - The list to add "None" to
|
||||
* @returns {Array} A new array with "None" option at the beginning
|
||||
*/
|
||||
const addNoneToList = list => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: t('AUTOMATION.NONE_OPTION') || 'None',
|
||||
},
|
||||
...(list || []),
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the condition dropdown values for a given type.
|
||||
* @param {string} type - The type of condition.
|
||||
@@ -95,6 +107,7 @@ export default function useAutomationValues() {
|
||||
slaPolicies: slaPolicies.value,
|
||||
languages,
|
||||
type,
|
||||
addNoneToListFn: addNoneToList,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'user:logout': this.onLogout,
|
||||
'page:reload': this.onReload,
|
||||
'assignee.changed': this.onAssigneeChanged,
|
||||
'team.changed': this.onTeamChanged,
|
||||
'conversation.typing_on': this.onTypingOn,
|
||||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.contact_changed': this.onConversationContactChange,
|
||||
@@ -77,14 +76,6 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onTeamChanged = payload => {
|
||||
const { id } = payload;
|
||||
if (id) {
|
||||
this.app.$store.dispatch('updateConversation', payload);
|
||||
}
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onConversationCreated = data => {
|
||||
this.app.$store.dispatch('addConversation', data);
|
||||
this.fetchConversationStats();
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
/**
|
||||
* Default agent object representing 'None'
|
||||
* @type {Object}
|
||||
*/
|
||||
export const createNoneAgent = {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters and sorts agents by availability status
|
||||
* @param {Array} agents - List of agents
|
||||
@@ -62,22 +49,3 @@ export const getAgentsByUpdatedPresence = (
|
||||
);
|
||||
return agentsWithDynamicPresenceUpdate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the filtered agents with the 'None' agent option if applicable.
|
||||
*
|
||||
* @param {Array} filteredAgentsByAvailability - The list of agents sorted by availability.
|
||||
* @param {boolean} includeNoneAgent - Whether to include the 'None' agent option.
|
||||
* @param {boolean} isAgentSelected - Whether an agent is currently selected.
|
||||
* @returns {Array} The combined list of agents, potentially including the 'None' agent.
|
||||
*/
|
||||
export const getCombinedAgents = (
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected
|
||||
) => {
|
||||
return [
|
||||
...(includeNoneAgent && isAgentSelected ? [createNoneAgent] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -87,6 +87,7 @@ export const generateCustomAttributeTypes = (customAttributes, type) => {
|
||||
};
|
||||
|
||||
export const generateConditionOptions = (options, key = 'id') => {
|
||||
if (!options || !Array.isArray(options)) return [];
|
||||
return options.map(i => {
|
||||
return {
|
||||
id: i[key],
|
||||
@@ -95,25 +96,17 @@ export const generateConditionOptions = (options, key = 'id') => {
|
||||
});
|
||||
};
|
||||
|
||||
// Add the "None" option to the agent list
|
||||
export const addNoneToList = agents => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
...(agents || []),
|
||||
];
|
||||
|
||||
export const getActionOptions = ({
|
||||
agents,
|
||||
teams,
|
||||
labels,
|
||||
slaPolicies,
|
||||
type,
|
||||
addNoneToListFn,
|
||||
}) => {
|
||||
const actionsMap = {
|
||||
assign_agent: addNoneToList(agents),
|
||||
assign_team: addNoneToList(teams),
|
||||
assign_agent: addNoneToListFn ? addNoneToListFn(agents) : agents,
|
||||
assign_team: addNoneToListFn ? addNoneToListFn(teams) : teams,
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
getAgentsByAvailability,
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
getCombinedAgents,
|
||||
createNoneAgent,
|
||||
} from '../agentHelper';
|
||||
import {
|
||||
allAgentsData,
|
||||
@@ -93,39 +91,4 @@ describe('agentHelper', () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedAgents', () => {
|
||||
it('includes None agent when includeNoneAgent is true and isAgentSelected is true', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, true);
|
||||
expect(result).toEqual([createNoneAgent, ...sortedByAvailability]);
|
||||
expect(result.length).toBe(sortedByAvailability.length + 1);
|
||||
expect(result[0]).toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, true);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when isAgentSelected is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('returns only filtered agents when both includeNoneAgent and isAgentSelected are false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
});
|
||||
|
||||
it('handles empty filteredAgentsByAvailability array', () => {
|
||||
const result = getCombinedAgents([], true, true);
|
||||
expect(result).toEqual([createNoneAgent]);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +118,46 @@ describe('getActionOptions', () => {
|
||||
expectedOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('adds None option when addNoneToListFn is provided', () => {
|
||||
const mockAddNoneToListFn = list => [
|
||||
{ id: 'nil', name: 'None' },
|
||||
...(list || []),
|
||||
];
|
||||
|
||||
const agents = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
const expectedOptions = [
|
||||
{ id: 'nil', name: 'None' },
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
expect(
|
||||
helpers.getActionOptions({
|
||||
agents,
|
||||
type: 'assign_agent',
|
||||
addNoneToListFn: mockAddNoneToListFn,
|
||||
})
|
||||
).toEqual(expectedOptions);
|
||||
});
|
||||
|
||||
it('does not add None option when addNoneToListFn is not provided', () => {
|
||||
const agents = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
expect(
|
||||
helpers.getActionOptions({
|
||||
agents,
|
||||
type: 'assign_agent',
|
||||
})
|
||||
).toEqual(agents);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionOptions', () => {
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "اختر وكيل",
|
||||
"TEAM": "اختر فريق"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "لا شيء"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "لم يتم العثور على وكلاء",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Изберете агент",
|
||||
"TEAM": "Изберете екип"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Нито един"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Няма намерени агенти",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"HEADER": "Agents",
|
||||
"HEADER_BTN_TXT": "Afegir Agent",
|
||||
"LOADING": "S'està recollint la llista d'agents",
|
||||
"DESCRIPTION": "An agent is a member of your customer support team who can view and respond to user messages. The list below shows all the agents in your account.",
|
||||
"LEARN_MORE": "Learn about user roles",
|
||||
"DESCRIPTION": "",
|
||||
"LEARN_MORE": "Rakibkazi",
|
||||
"AGENT_TYPES": {
|
||||
"ADMINISTRATOR": "Administrador/a",
|
||||
"AGENT": "Agent"
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Seleccionar Agent",
|
||||
"TEAM": "Selecciona equip"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Ningú"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No s'han trobat agents",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Vybrat agenta",
|
||||
"TEAM": "Vybrat tým"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nic"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nenalezeni žádní agenti",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Vælg agent",
|
||||
"TEAM": "Vælg hold"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Ingen"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Ingen agenter fundet",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Agent auswählen",
|
||||
"TEAM": "Team auswählen"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Keine"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Keine Agenten gefunden",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Επιλογή πράκτορα",
|
||||
"TEAM": "Επιλογή ομάδας"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Κανένα"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Δεν βρέθηκαν Πράκτορες",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
|
||||
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
|
||||
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
|
||||
}
|
||||
},
|
||||
"NONE_OPTION": "None"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"NO_INBOX_2": " to get started",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Looks like you are not part of any inbox. Please contact your administrator",
|
||||
"SEARCH_MESSAGES": "Search for messages in conversations",
|
||||
"VIEW_ORIGINAL": "View original",
|
||||
"VIEW_TRANSLATED": "View translated",
|
||||
"EMPTY_STATE": {
|
||||
"CMD_BAR": "to open command menu",
|
||||
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Seleccionar agente",
|
||||
"TEAM": "Seleccionar equipo"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Ninguna"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No se encontraron agentes",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "انتخاب اپراتور",
|
||||
"TEAM": "انتخاب تیم"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "هیچکدام"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "اپراتوری یافت نشد",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Edustajia ei löytynyt",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Sélectionner un agent",
|
||||
"TEAM": "Sélectionner une équipe"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Aucun"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Aucun agent trouvé",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "בחר נציג",
|
||||
"TEAM": "בחר קבוצה"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "כלום"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "לא נמצא סוכן",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nijedno"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Ügynök kiválasztása",
|
||||
"TEAM": "Csapat kiválasztása"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nincs"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nem találunk ügynököt",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Pilih Agen",
|
||||
"TEAM": "Pilih tim"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Tidak ada"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Tidak ada agen",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Velja þjónustufulltrúa",
|
||||
"TEAM": "Velja teymi"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Enginn"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Engir þjónustufulltrúar fundust",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Seleziona un'agente",
|
||||
"TEAM": "Seleziona team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nessuno"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nessun agente trovato",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "担当者を選択",
|
||||
"TEAM": "チームを選択"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "なし"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "担当者が見つかりません",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "에이전트 선택",
|
||||
"TEAM": "팀 선택"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "없음"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "에이전트를 찾을 수 없음",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Pasirinkti agentą",
|
||||
"TEAM": "Pasirinkite komandą"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nėra"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Agentų nerasta",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Izvēlieties aģentu",
|
||||
"TEAM": "Izvēlieties komandu"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nav"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Aģenti nav atrasti",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "ഏജന്റിനെ തിരഞ്ഞെടുക്കുക",
|
||||
"TEAM": "ടീം തിരഞ്ഞെടുക്കുക"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "ഒന്നുമില്ല"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "ഏജന്റകളെ ഒന്നും കണ്ടെത്താൻ സാധിച്ചില്ല",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Pilih ejen",
|
||||
"TEAM": "Pilih pasukan"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Tiada"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Tiada ejen dijumpa",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Selecteer agent",
|
||||
"TEAM": "Selecteer team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Geen"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Geen medewerkers gevonden",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Velg agent",
|
||||
"TEAM": "Velg gruppe"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Ingen"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Ingen agenter funnet",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Wybierz konsultanta",
|
||||
"TEAM": "Wybierz zespół"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Brak"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nie znaleziono agentów",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Escolher agente",
|
||||
"TEAM": "Escolher equipa"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nenhuma"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nenhum agente encontrado",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Selecionar agente",
|
||||
"TEAM": "Selecionar time"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nenhuma"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nenhum agente encontrado",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"HEADER": "Auditoria",
|
||||
"HEADER_BTN_TXT": "Adicionar Logs de Auditoria",
|
||||
"LOADING": "Buscando Logs de Auditoria",
|
||||
"DESCRIPTION": "Logs de Auditoria mantêm um registro de atividades em sua conta, permitindo que você acompanhe e auditoria de sua conta, equipe ou serviços.",
|
||||
"DESCRIPTION": "Logs de Auditoria mantêm um registro de atividades em sua conta, permitindo que você acompanhe e auditore sua conta, equipe ou serviços.",
|
||||
"LEARN_MORE": "Saiba mais sobre os logs de auditoria",
|
||||
"SEARCH_404": "Não existem itens correspondentes a esta consulta",
|
||||
"SIDEBAR_TXT": "<p><b>Logs de Auditoria</b> </p><p> Os Logs de Auditoria são rastros para eventos e ações em um Sistema Chatwoot. </p>",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Selectați agentul",
|
||||
"TEAM": "Selectați echipa"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Nimic"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Niciun agent găsit",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Выбрать Агента",
|
||||
"TEAM": "Выберите команду"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Ничего"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Операторы не найдены",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Vybrať agenta",
|
||||
"TEAM": "Vybrať tím"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Žiadne"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Žiadni agenti neboli nájdení",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Izaberite agenta",
|
||||
"TEAM": "Izaberite tim"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Niko"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Nema pronađenih agenata",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Välj agent",
|
||||
"TEAM": "Välj team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Inget"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Inga agenter hittades",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "เลือกพนักงาน",
|
||||
"TEAM": "เลือกทีม"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "ไม่มี"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "ไม่พบพนักงาน",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Temsilci seçin",
|
||||
"TEAM": "Takım seçin"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Hiç"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Kullanıcı bulunamadı",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Виберіть агента",
|
||||
"TEAM": "Виберіть команду"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Нiчого"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Агентів не знайдено",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "ایجنٹ منتخب کریں۔",
|
||||
"TEAM": "ٹیم منتخب کریں۔"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "کوئی نہیں۔"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "کوئی ایجنٹ نہیں ملا",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "Chọn thành viên",
|
||||
"TEAM": "Chọn nhóm"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "Không có"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "Không tìm thấy nhà cung cấp",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "选择客服代表",
|
||||
"TEAM": "选择团队"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "啥都没有"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "未找到客服代表",
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"AGENT": "選擇客服",
|
||||
"TEAM": "選擇團隊"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "無"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "查無客服",
|
||||
|
||||
@@ -11,14 +11,12 @@ import {
|
||||
ACCOUNT_EVENTS,
|
||||
CONVERSATION_EVENTS,
|
||||
} from '../../../helper/AnalyticsHelper/events';
|
||||
import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal.vue';
|
||||
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCannedModal,
|
||||
TranslateModal,
|
||||
MenuItem,
|
||||
ContextMenu,
|
||||
},
|
||||
@@ -54,7 +52,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isCannedResponseModalOpen: false,
|
||||
showTranslateModal: false,
|
||||
showDeleteModal: false,
|
||||
};
|
||||
},
|
||||
@@ -125,15 +122,11 @@ export default {
|
||||
});
|
||||
useTrack(CONVERSATION_EVENTS.TRANSLATE_A_MESSAGE);
|
||||
this.handleClose();
|
||||
this.showTranslateModal = true;
|
||||
},
|
||||
handleReplyTo() {
|
||||
this.$emit('replyTo', this.message);
|
||||
this.handleClose();
|
||||
},
|
||||
onCloseTranslateModal() {
|
||||
this.showTranslateModal = false;
|
||||
},
|
||||
openDeleteModal() {
|
||||
this.handleClose();
|
||||
this.showDeleteModal = true;
|
||||
@@ -170,13 +163,6 @@ export default {
|
||||
:on-close="hideCannedResponseModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
<!-- Translate Content -->
|
||||
<TranslateModal
|
||||
v-if="showTranslateModal"
|
||||
:content="messageContent"
|
||||
:content-attributes="contentAttributes"
|
||||
@close="onCloseTranslateModal"
|
||||
/>
|
||||
<!-- Confirm Deletion -->
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
|
||||
@@ -251,7 +251,7 @@ export default {
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.TEAM')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.INPUT')
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.TEAM')
|
||||
"
|
||||
@select="onClickAssignTeam"
|
||||
/>
|
||||
|
||||
@@ -54,11 +54,9 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between items-center h-14 min-h-[3.5rem] px-4 py-2 bg-n-background border-b border-n-weak"
|
||||
class="flex justify-between items-center h-20 min-h-[3.5rem] px-4 py-2 bg-n-background"
|
||||
>
|
||||
<h1
|
||||
class="flex items-center mb-0 text-2xl text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<h1 class="flex items-center mb-0 text-2xl text-n-slate-12">
|
||||
<woot-sidemenu-icon v-if="showSidemenuIcon" />
|
||||
<BackButton
|
||||
v-if="showBackButton"
|
||||
@@ -66,21 +64,16 @@ export default {
|
||||
:back-url="backUrl"
|
||||
class="ml-2 mr-4"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
:class="iconClass"
|
||||
class="hidden ml-1 mr-2 rtl:ml-2 rtl:mr-1 md:block"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
<span class="text-2xl font-medium text-slate-900 dark:text-slate-100">
|
||||
<span class="text-xl font-medium text-slate-900 dark:text-slate-100">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
</h1>
|
||||
<router-link
|
||||
v-if="showNewButton && isAdmin"
|
||||
:to="buttonRoute"
|
||||
class="button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
class="button success button--fixed-top rounded-[5px] flex gap-2"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<span class="button__content">
|
||||
|
||||
@@ -17,29 +17,31 @@ const props = defineProps({
|
||||
const { t } = useI18n();
|
||||
|
||||
const showNewButton = computed(
|
||||
() => props.newButtonRoutes.length !== 0 && !props.showBackButton
|
||||
() => props.newButtonRoutes.length && !props.showBackButton
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-n-background overflow-auto"
|
||||
>
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
:header-title="t(headerTitle)"
|
||||
:button-text="t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
:show-sidemenu-icon="showSidemenuIcon"
|
||||
/>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="keepAlive">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-else />
|
||||
</router-view>
|
||||
<div class="flex flex-1 flex-col m-0 bg-n-background overflow-auto">
|
||||
<div class="max-w-6xl mx-auto w-full flex flex-col flex-1">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
:header-title="t(headerTitle)"
|
||||
:button-text="t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
:show-sidemenu-icon="showSidemenuIcon"
|
||||
class="sticky top-0 z-20"
|
||||
/>
|
||||
|
||||
<router-view v-slot="{ Component }" class="px-5 flex-1 overflow-hidden">
|
||||
<component :is="Component" v-if="!keepAlive" :key="$route.fullPath" />
|
||||
<keep-alive v-else>
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,8 +9,14 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseSettingsHeader,
|
||||
V4Button,
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const { enabledLanguages } = useConfig();
|
||||
@@ -161,131 +167,130 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink min-w-0 p-6 overflow-auto">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex flex-row p-4 border-b border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<div class="flex flex-col w-full">
|
||||
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')">
|
||||
<template #actions>
|
||||
<V4Button blue :loading="isUpdating" @click="updateAccount">
|
||||
{{ $t('GENERAL_SETTINGS.SUBMIT') }}
|
||||
</V4Button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="flex-grow flex-shrink min-w-0 overflow-auto mt-3">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
class="flex flex-row border-b border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="flex flex-row p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="build-id">
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-top"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
:loading="isUpdating"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
<div class="flex flex-row">
|
||||
<div class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0">
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="build-id">
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import SettingsContent from '../Wrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -9,12 +9,7 @@ export default {
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'GENERAL_SETTINGS.TITLE',
|
||||
icon: 'briefcase',
|
||||
showNewButton: false,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
+6
-8
@@ -41,7 +41,7 @@ const openInNewTab = url => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-2 pt-4">
|
||||
<div class="flex flex-col items-start w-full gap-2">
|
||||
<BackButton
|
||||
v-if="backButtonLabel"
|
||||
compact
|
||||
@@ -60,13 +60,11 @@ const openInNewTab = url => {
|
||||
size="14"
|
||||
:icon="iconName"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
class="flex-shrink-0 text-n-brand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1
|
||||
class="text-2xl font-semibold font-interDisplay tracking-[0.3px] text-slate-900 dark:text-slate-25"
|
||||
>
|
||||
<h1 class="text-xl font-medium tracking-tight text-n-slate-12">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</div>
|
||||
@@ -75,9 +73,9 @@ const openInNewTab = url => {
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-3 text-slate-600 dark:text-slate-300">
|
||||
<div class="flex flex-col w-full gap-3 text-n-slate-11">
|
||||
<p
|
||||
class="mb-0 text-base font-normal line-clamp-5 sm:line-clamp-none max-w-3xl tracking-[-0.1px]"
|
||||
class="mb-0 text-sm font-normal line-clamp-5 sm:line-clamp-none max-w-3xl"
|
||||
>
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
@@ -87,7 +85,7 @@ const openInNewTab = url => {
|
||||
:href="helpURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="items-center hidden gap-1 text-sm font-medium sm:inline-flex w-fit text-n-brand dark:text-n-brand hover:underline"
|
||||
class="items-center hidden gap-1 text-sm font-medium sm:inline-flex w-fit text-n-brand hover:underline"
|
||||
>
|
||||
{{ linkText }}
|
||||
<Icon
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex relative flex-col sm:flex-row p-4 gap-4 sm:p-6 justify-between shadow-sm group bg-white border border-solid rounded-xl dark:bg-slate-800 border-slate-75 dark:border-slate-700/50 w-full"
|
||||
class="flex relative flex-col sm:flex-row p-4 gap-4 sm:p-6 justify-between group outline outline-n-container outline-1 bg-n-alpha-3 rounded-2xl shadow w-full"
|
||||
>
|
||||
<slot name="leftSection">
|
||||
<div class="flex flex-col min-w-0 items-start gap-3 max-w-[480px] w-full">
|
||||
@@ -21,7 +21,7 @@ defineProps({
|
||||
class="flex items-center justify-between w-full gap-3 sm:justify-normal whitespace-nowrap"
|
||||
>
|
||||
<h3
|
||||
class="justify-between text-sm font-medium truncate w-fit sm:justify-normal text-slate-900 dark:text-slate-25"
|
||||
class="justify-between tracking-tight font-medium truncate w-fit sm:justify-normal text-slate-900 dark:text-slate-25"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
|
||||
@@ -40,9 +40,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row overflow-auto p-4 h-full bg-n-alpha-2 dark:bg-n-solid-1"
|
||||
>
|
||||
<div class="flex flex-row overflow-auto h-full">
|
||||
<woot-wizard
|
||||
class="hidden md:block w-1/4"
|
||||
:global-config="globalConfig"
|
||||
|
||||
@@ -360,7 +360,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow flex-shrink w-full min-w-0 pl-0 pr-0 overflow-auto settings bg-n-solid-1"
|
||||
class="flex-grow flex-shrink w-full min-w-0 pl-0 pr-0 overflow-auto settings"
|
||||
>
|
||||
<SettingIntroBanner
|
||||
:header-image="inbox.avatarUrl"
|
||||
|
||||
@@ -1,78 +1,76 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationName: { type: String, default: '' },
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
integrationAction: { type: String, default: '' },
|
||||
actionButtonText: { type: String, default: '' },
|
||||
deleteConfirmationText: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
openDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.closeDeletePopup();
|
||||
this.deleteIntegration(this.deleteIntegration);
|
||||
this.$router.push({ name: 'settings_integrations' });
|
||||
},
|
||||
async deleteIntegration() {
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
'integrations/deleteIntegration',
|
||||
this.integrationId
|
||||
);
|
||||
useAlert(this.$t('INTEGRATION_SETTINGS.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
const props = defineProps({
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationName: { type: String, default: '' },
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
integrationAction: { type: String, default: '' },
|
||||
actionButtonText: { type: String, default: '' },
|
||||
deleteConfirmationText: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
|
||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||
const globalConfig = computed(() => store.getters['globalConfig/get']);
|
||||
|
||||
const openDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = true;
|
||||
};
|
||||
|
||||
const closeDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = false;
|
||||
};
|
||||
|
||||
const deleteIntegration = async () => {
|
||||
try {
|
||||
await store.dispatch('integrations/deleteIntegration', props.integrationId);
|
||||
useAlert('INTEGRATION_SETTINGS.DELETE.API.SUCCESS_MESSAGE');
|
||||
} catch (error) {
|
||||
useAlert('INTEGRATION_SETTINGS.WEBHOOK.DELETE.API.ERROR_MESSAGE');
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
closeDeletePopup();
|
||||
deleteIntegration();
|
||||
router.push({ name: 'settings_integrations' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start justify-between md:flex-row md:items-center"
|
||||
class="flex flex-col items-start justify-between md:flex-row md:items-center p-4 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow"
|
||||
>
|
||||
<div class="flex items-center justify-start flex-1 m-0 mx-4">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
class="w-16 h-16 p-2 mr-4"
|
||||
/>
|
||||
<div class="flex items-center justify-start flex-1 m-0 mx-4 gap-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}-dark.png`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-1 text-xl font-medium text-slate-800 dark:text-slate-100">
|
||||
<h3 class="mb-1 text-xl font-medium text-n-slate-12">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-200">
|
||||
<p class="text-n-slate-11 text-sm leading-6">
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
|
||||
+3
-1
@@ -108,7 +108,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-auto p-4 max-w-full my-auto flex flex-wrap h-full">
|
||||
<div
|
||||
class="overflow-auto p-4 max-w-6xl mx-auto my-auto flex flex-wrap h-full"
|
||||
>
|
||||
<woot-button
|
||||
v-if="showAddButton"
|
||||
color-scheme="success"
|
||||
|
||||
+11
-11
@@ -37,7 +37,7 @@ const integrationStatus = computed(() =>
|
||||
);
|
||||
|
||||
const integrationStatusColor = computed(() =>
|
||||
props.enabled ? 'bg-green-500' : 'bg-slate-200'
|
||||
props.enabled ? 'bg-n-teal-9' : 'bg-n-slate-8'
|
||||
);
|
||||
|
||||
const actionURL = computed(() =>
|
||||
@@ -47,30 +47,30 @@ const actionURL = computed(() =>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col flex-1 p-6 bg-white border border-solid rounded-md dark:bg-slate-800 border-slate-50 dark:border-slate-700/50"
|
||||
class="flex flex-col flex-1 p-6 m-[1px] outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex h-12 w-12 mb-4">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm block dark:hidden bg-white dark:bg-slate-900"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}-dark.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm hidden dark:block bg-white dark:bg-slate-900"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
<fluent-icon
|
||||
<span
|
||||
v-tooltip="integrationStatus"
|
||||
size="20"
|
||||
class="text-white p-0.5 rounded-full"
|
||||
class="text-white p-0.5 rounded-full w-5 h-5 flex items-center justify-center"
|
||||
:class="integrationStatusColor"
|
||||
icon="checkmark"
|
||||
/>
|
||||
>
|
||||
<i class="i-ph-check-bold text-sm" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col m-0 flex-1">
|
||||
<div
|
||||
class="font-medium mb-2 text-slate-800 dark:text-slate-100 flex justify-between items-center"
|
||||
class="font-medium mb-2 text-n-slate-12 flex justify-between items-center"
|
||||
>
|
||||
<span class="text-base font-semibold">{{ name }}</span>
|
||||
<router-link :to="actionURL">
|
||||
@@ -79,7 +79,7 @@ const actionURL = computed(() =>
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-200">
|
||||
<p class="text-n-slate-11">
|
||||
{{ useInstallationName(description, globalConfig.installationName) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -35,32 +35,23 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingLinear"
|
||||
class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.LINEAR.DELETE.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.LINEAR.DELETE.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto max-w-6xl mx-auto">
|
||||
<div v-if="integrationLoaded && !uiFlags.isCreatingLinear">
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.LINEAR.DELETE.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.LINEAR.DELETE.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+13
-25
@@ -47,31 +47,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div
|
||||
v-if="integrationLoaded"
|
||||
class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="integration.enabled"
|
||||
class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<IntegrationHelpText />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-6xl">
|
||||
<div v-if="integrationLoaded">
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="integration.enabled">
|
||||
<IntegrationHelpText />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+52
-53
@@ -1,63 +1,62 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
export default {
|
||||
props: {
|
||||
integrationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
integrationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emits: ['add', 'delete'],
|
||||
setup(props) {
|
||||
const { integration, hasConnectedHooks } = useIntegrationHook(
|
||||
props.integrationId
|
||||
);
|
||||
return { integration, hasConnectedHooks };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
defineEmits(['add', 'delete']);
|
||||
|
||||
const { integration, hasConnectedHooks } = useIntegrationHook(
|
||||
props.integrationId
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-shrink flex-grow overflow-auto p-4">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 border border-solid border-slate-75 dark:border-slate-700/50 rounded-xl mb-4 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex h-[6.25rem] w-[6.25rem]">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integration.id}.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm block dark:hidden bg-white dark:bg-slate-900"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integration.id}-dark.png`"
|
||||
class="max-w-full rounded-md border border-slate-50 dark:border-slate-700/50 shadow-sm hidden dark:block bg-white dark:bg-slate-900"
|
||||
<div
|
||||
class="outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow flex-grow overflow-auto p-4"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex h-16 w-16 items-center justify-center">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}-dark.png`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center m-0 mx-4 flex-1">
|
||||
<h3 class="mb-1 text-xl font-medium text-n-slate-12">
|
||||
{{ integration.name }}
|
||||
</h3>
|
||||
<p class="text-n-slate-11 text-sm leading-6">
|
||||
{{ integration.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<div v-if="hasConnectedHooks">
|
||||
<div @click="$emit('delete', integration.hooks[0])">
|
||||
<Button
|
||||
ruby
|
||||
faded
|
||||
:label="$t('INTEGRATION_APPS.DISCONNECT.BUTTON_TEXT')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center m-0 mx-4 flex-1">
|
||||
<h3
|
||||
class="text-xl font-medium mb-1 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ integration.name }}
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-200">
|
||||
{{ integration.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<div v-if="hasConnectedHooks">
|
||||
<div @click="$emit('delete', integration.hooks[0])">
|
||||
<woot-button class="nice alert">
|
||||
{{ $t('INTEGRATION_APPS.DISCONNECT.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<woot-button class="button nice" @click="$emit('add')">
|
||||
{{ $t('INTEGRATION_APPS.CONNECT.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button
|
||||
blue
|
||||
faded
|
||||
:label="$t('INTEGRATION_APPS.CONNECT.BUTTON_TEXT')"
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,102 +1,99 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Integration from './Integration.vue';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
Integration,
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return { integrationLoaded: false };
|
||||
},
|
||||
computed: {
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('slack');
|
||||
},
|
||||
areHooksAvailable() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
return !!hooks.length;
|
||||
},
|
||||
hook() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
const [hook] = hooks;
|
||||
return hook || {};
|
||||
},
|
||||
isIntegrationHookEnabled() {
|
||||
return this.hook.status || false;
|
||||
},
|
||||
hasConnectedAChannel() {
|
||||
return !!this.hook.reference_id;
|
||||
},
|
||||
selectedChannelName() {
|
||||
if (this.hook.status) {
|
||||
const { settings: { channel_name: channelName = '' } = {} } = this.hook;
|
||||
return channelName || 'customer-conversations';
|
||||
}
|
||||
return this.$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.SELECTED');
|
||||
},
|
||||
...mapGetters({
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
|
||||
integrationAction() {
|
||||
if (this.integration.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
},
|
||||
methods: {
|
||||
async intializeSlackIntegration() {
|
||||
await this.$store.dispatch('integrations/get', 'slack');
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// Clear the query param `code` from the URL as the
|
||||
// subsequent reloads would result in an error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
const props = defineProps({
|
||||
code: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const integrationLoaded = ref(false);
|
||||
|
||||
const integration = computed(() => {
|
||||
return store.getters['integrations/getIntegration']('slack');
|
||||
});
|
||||
|
||||
const areHooksAvailable = computed(() => {
|
||||
const { hooks = [] } = integration.value || {};
|
||||
return !!hooks.length;
|
||||
});
|
||||
|
||||
const hook = computed(() => {
|
||||
const { hooks = [] } = integration.value || {};
|
||||
const [firstHook] = hooks;
|
||||
return firstHook || {};
|
||||
});
|
||||
|
||||
const isIntegrationHookEnabled = computed(() => {
|
||||
return hook.value.status || false;
|
||||
});
|
||||
|
||||
const hasConnectedAChannel = computed(() => {
|
||||
return !!hook.value.reference_id;
|
||||
});
|
||||
|
||||
const selectedChannelName = computed(() => {
|
||||
if (hook.value.status) {
|
||||
const { settings: { channel_name: channelName = '' } = {} } = hook.value;
|
||||
return channelName || 'customer-conversations';
|
||||
}
|
||||
return t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.SELECTED');
|
||||
});
|
||||
|
||||
const uiFlags = computed(() => store.getters['integrations/getUIFlags']);
|
||||
|
||||
const integrationAction = computed(() => {
|
||||
if (integration.value.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return integration.value.action;
|
||||
});
|
||||
|
||||
const intializeSlackIntegration = async () => {
|
||||
await store.dispatch('integrations/get', 'slack');
|
||||
if (props.code) {
|
||||
await store.dispatch('integrations/connectSlack', props.code);
|
||||
// Clear the query param `code` from the URL as the
|
||||
// subsequent reloads would result in an error
|
||||
router.replace(route.path);
|
||||
}
|
||||
integrationLoaded.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
intializeSlackIntegration();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingSlack"
|
||||
class="flex flex-col flex-1 overflow-auto"
|
||||
class="flex flex-col flex-1 overflow-auto gap-5 pt-1 pb-10"
|
||||
>
|
||||
<div
|
||||
class="p-4 bg-white border-b border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="areHooksAvailable" class="flex-1 p-6">
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
<div v-if="areHooksAvailable" class="flex-1">
|
||||
<SelectChannelWarning
|
||||
v-if="!isIntegrationHookEnabled"
|
||||
:has-connected-a-channel="hasConnectedAChannel"
|
||||
|
||||
+81
-90
@@ -1,109 +1,99 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
hasConnectedAChannel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
errorDescription() {
|
||||
return !this.hasConnectedAChannel
|
||||
? this.$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.DESCRIPTION')
|
||||
: this.$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.EXPIRED');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchChannels() {
|
||||
try {
|
||||
this.availableChannels = await this.$store.dispatch(
|
||||
'integrations/listAllSlackChannels'
|
||||
);
|
||||
this.availableChannels.sort((c1, c2) => c1.name - c2.name);
|
||||
} catch {
|
||||
this.$t('INTEGRATION_SETTINGS.SLACK.FAILED_TO_FETCH_CHANNELS');
|
||||
this.availableChannels = [];
|
||||
}
|
||||
},
|
||||
async updateIntegration() {
|
||||
try {
|
||||
await this.$store.dispatch('integrations/updateSlack', {
|
||||
referenceId: this.selectedChannelId,
|
||||
});
|
||||
useAlert(this.$t('INTEGRATION_SETTINGS.SLACK.UPDATE_SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(error.message || 'INTEGRATION_SETTINGS.SLACK.UPDATE_ERROR');
|
||||
}
|
||||
},
|
||||
const props = defineProps({
|
||||
hasConnectedAChannel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const selectedChannelId = ref('');
|
||||
const availableChannels = ref([]);
|
||||
|
||||
const uiFlags = computed(() => store.getters['integrations/getUIFlags']);
|
||||
|
||||
const errorDescription = computed(() => {
|
||||
return !props.hasConnectedAChannel
|
||||
? t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.DESCRIPTION')
|
||||
: t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.EXPIRED');
|
||||
});
|
||||
const globalConfig = computed(() => store.getters['globalConfig/get']);
|
||||
|
||||
const formattedErrorMessage = computed(() => {
|
||||
return formatMessage(
|
||||
useInstallationName(
|
||||
errorDescription.value,
|
||||
globalConfig.value.installationName
|
||||
),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
const fetchChannels = async () => {
|
||||
try {
|
||||
availableChannels.value = await store.dispatch(
|
||||
'integrations/listAllSlackChannels'
|
||||
);
|
||||
availableChannels.value.sort((c1, c2) => c1.name - c2.name);
|
||||
} catch {
|
||||
t('INTEGRATION_SETTINGS.SLACK.FAILED_TO_FETCH_CHANNELS');
|
||||
availableChannels.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const updateIntegration = async () => {
|
||||
try {
|
||||
await store.dispatch('integrations/updateSlack', {
|
||||
referenceId: selectedChannelId.value,
|
||||
});
|
||||
useAlert(t('INTEGRATION_SETTINGS.SLACK.UPDATE_SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(error.message || 'INTEGRATION_SETTINGS.SLACK.UPDATE_ERROR');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-6 py-4 mb-4 border border-yellow-200 rounded-md bg-yellow-50 dark:border-slate-700 dark:bg-slate-800"
|
||||
class="px-6 py-4 mb-4 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<fluent-icon
|
||||
icon="alert"
|
||||
class="text-yellow-500 dark:text-yellow-400"
|
||||
size="24"
|
||||
/>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="i-lucide-bell text-xl text-n-amber-11 mt-1" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p
|
||||
class="mb-1 text-base font-semibold text-yellow-900 dark:text-yellow-500"
|
||||
>
|
||||
<p class="mb-1 text-base font-semibold text-n-slate-12">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.ATTENTION_REQUIRED')
|
||||
}}
|
||||
</p>
|
||||
<div class="mt-2 text-sm text-yellow-800 dark:text-yellow-600">
|
||||
<p
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
useInstallationName(
|
||||
errorDescription,
|
||||
globalConfig.installationName
|
||||
),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-n-slate-11 mb-3">
|
||||
<p v-dompurify-html="formattedErrorMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hasConnectedAChannel" class="mt-2 ml-8">
|
||||
<woot-submit-button
|
||||
<div v-if="!hasConnectedAChannel" class="mb-2 mt-1 ml-8">
|
||||
<Button
|
||||
v-if="!availableChannels.length"
|
||||
button-class="smooth small warning"
|
||||
:loading="uiFlags.isFetchingSlackChannels"
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.BUTTON_TEXT')
|
||||
"
|
||||
spinner-class="warning"
|
||||
amber
|
||||
sm
|
||||
:is-loading="uiFlags.isFetchingSlackChannels"
|
||||
@click="fetchChannels"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.BUTTON_TEXT') }}
|
||||
</Button>
|
||||
<div v-else class="inline-flex">
|
||||
<select
|
||||
v-model="selectedChannelId"
|
||||
@@ -120,13 +110,14 @@ export default {
|
||||
#{{ channel.name }}
|
||||
</option>
|
||||
</select>
|
||||
<woot-submit-button
|
||||
button-class="smooth small success"
|
||||
:button-text="$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.UPDATE')"
|
||||
spinner-class="success"
|
||||
:loading="uiFlags.isUpdatingSlack"
|
||||
<Button
|
||||
teal
|
||||
sm
|
||||
:is-loading="uiFlags.isUpdatingSlack"
|
||||
@click="updateIntegration"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.UPDATE') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+25
-29
@@ -1,41 +1,37 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
export default {
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
const props = defineProps({
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const formattedHelpText = computed(() => {
|
||||
return formatMessage(
|
||||
t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY', {
|
||||
selectedChannelName: props.selectedChannelName,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 w-full p-6 bg-white rounded-md border border-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
class="flex-1 w-full px-6 py-5 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow"
|
||||
>
|
||||
<div class="prose-lg max-w-5xl">
|
||||
<h5 class="dark:text-slate-100">
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.TITLE') }}
|
||||
<h5 class="text-n-slate-12 tracking-tight">
|
||||
{{ t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.TITLE') }}
|
||||
</h5>
|
||||
<p>
|
||||
<span
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY', {
|
||||
selectedChannelName: selectedChannelName,
|
||||
}),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
<div v-dompurify-html="formattedHelpText" class="text-n-slate-11" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,9 +29,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="p-3 bg-white dark:bg-slate-900 h-[calc(100vh-3.5rem)] flex flex-col border-l border-n-weak"
|
||||
>
|
||||
<div class="p-3 bg-n-background h-full flex flex-col">
|
||||
<div>
|
||||
<woot-input
|
||||
:model-value="macroName"
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="overflow-auto p-4 max-w-full my-auto flex flex-row flex-nowrap h-full bg-slate-25 dark:bg-slate-800"
|
||||
class="overflow-auto p-4 max-w-full my-auto flex flex-row flex-nowrap h-full"
|
||||
>
|
||||
<woot-wizard class="hidden md:block w-1/4" :items="items" />
|
||||
<router-view />
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="overflow-auto p-4 max-w-full my-auto flex flex-row flex-nowrap h-full bg-slate-25 dark:bg-slate-800"
|
||||
class="overflow-auto p-4 max-w-full my-auto flex flex-row flex-nowrap h-full"
|
||||
>
|
||||
<woot-wizard class="hidden md:block w-1/4" :items="items" />
|
||||
<router-view />
|
||||
|
||||
@@ -6,27 +6,7 @@ import { MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from '../../../../shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const makeCompactJson = obj => {
|
||||
// get rid of irrelevant data
|
||||
const {
|
||||
messages,
|
||||
additional_attributes,
|
||||
sla_events,
|
||||
contact_inbox,
|
||||
...remaining
|
||||
} = obj;
|
||||
const strinRep = JSON.stringify(remaining);
|
||||
|
||||
// replace active storage URLs with a placeholder
|
||||
const activeStorageUrlRegex =
|
||||
/https?:\/\/[^/]+\/rails\/active_storage[^\s]+/g;
|
||||
const replacedStr = strinRep.replace(
|
||||
activeStorageUrlRegex,
|
||||
'[ACTIVE_STORAGE_URL]'
|
||||
);
|
||||
return replacedStr;
|
||||
};
|
||||
import * as Sentry from '@sentry/vue';
|
||||
|
||||
const state = {
|
||||
allConversations: [],
|
||||
@@ -237,31 +217,19 @@ export const mutations = {
|
||||
|
||||
// ignore out of order events
|
||||
if (conversation.updated_at < selectedConversation.updated_at) {
|
||||
if (window.newrelic) {
|
||||
window.newrelic.log(
|
||||
`conversation update mismatch for ${conversation.id} with incoming ${makeCompactJson(conversation)} and stored ${makeCompactJson(selectedConversation)}`,
|
||||
{ level: 'DEBUG' }
|
||||
);
|
||||
}
|
||||
Sentry.withScope(scope => {
|
||||
scope.setContext('incoming', conversation);
|
||||
scope.setContext('stored', selectedConversation);
|
||||
scope.setContext('incoming_meta', conversation.meta);
|
||||
scope.setContext('stored_meta', selectedConversation.meta);
|
||||
Sentry.captureMessage('Conversation update mismatch');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// in case the timestamp is the same, we track it and let it update
|
||||
if (conversation.updated_at === selectedConversation.updated_at) {
|
||||
if (window.newrelic) {
|
||||
window.newrelic.log(
|
||||
`conversation update overlap for ${conversation.id} with incoming ${makeCompactJson(conversation)} and stored ${makeCompactJson(selectedConversation)}`,
|
||||
{ level: 'DEBUG' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.newrelic) {
|
||||
window.newrelic.log(
|
||||
`processing conversation update for ${conversation.id} with incoming ${makeCompactJson(conversation)} and stored ${makeCompactJson(selectedConversation)}`,
|
||||
{ level: 'DEBUG' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { messages, ...updates } = conversation;
|
||||
|
||||
@@ -569,4 +569,382 @@ describe('#mutations', () => {
|
||||
expect(state.copilotAssistant).toEqual(data.assistant);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_MESSAGES_LOADED', () => {
|
||||
it('should set allMessagesLoaded to true on selected chat', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, allMessagesLoaded: false }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
mutations[types.SET_ALL_MESSAGES_LOADED](state);
|
||||
expect(state.allConversations[0].allMessagesLoaded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_ALL_MESSAGES_LOADED', () => {
|
||||
it('should set allMessagesLoaded to false on selected chat', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, allMessagesLoaded: true }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
mutations[types.CLEAR_ALL_MESSAGES_LOADED](state);
|
||||
expect(state.allConversations[0].allMessagesLoaded).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_PREVIOUS_CONVERSATIONS', () => {
|
||||
it('should prepend messages to conversation messages array', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'msg1' }] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([
|
||||
{ id: 'msg1' },
|
||||
{ id: 'msg2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not modify messages if data is empty', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'msg2' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_MISSING_MESSAGES', () => {
|
||||
it('should replace message array with new data', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'old' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'new' }] };
|
||||
|
||||
mutations[types.SET_MISSING_MESSAGES](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'new' }]);
|
||||
});
|
||||
|
||||
it('should do nothing if conversation is not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'new' }] };
|
||||
|
||||
mutations[types.SET_MISSING_MESSAGES](state, payload);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ASSIGN_AGENT', () => {
|
||||
it('should assign agent to selected conversation', () => {
|
||||
const assignee = { id: 1, name: 'Agent' };
|
||||
const state = {
|
||||
allConversations: [{ id: 1, meta: {} }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.ASSIGN_AGENT](state, assignee);
|
||||
expect(state.allConversations[0].meta.assignee).toEqual(assignee);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ASSIGN_PRIORITY', () => {
|
||||
it('should assign priority to conversation', () => {
|
||||
const priority = { title: 'Urgent', value: 'urgent' };
|
||||
const state = {
|
||||
allConversations: [{ id: 1 }],
|
||||
};
|
||||
|
||||
mutations[types.ASSIGN_PRIORITY](state, {
|
||||
priority,
|
||||
conversationId: 1,
|
||||
});
|
||||
expect(state.allConversations[0].priority).toEqual(priority);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#MUTE_CONVERSATION', () => {
|
||||
it('should mute selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, muted: false }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.MUTE_CONVERSATION](state);
|
||||
expect(state.allConversations[0].muted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UNMUTE_CONVERSATION', () => {
|
||||
it('should unmute selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, muted: true }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.UNMUTE_CONVERSATION](state);
|
||||
expect(state.allConversations[0].muted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION', () => {
|
||||
it('should update existing conversation', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
messages: [{ id: 'msg1' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
messages: [{ id: 'msg2' }],
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0]).toEqual({
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
messages: [{ id: 'msg1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add conversation if not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([conversation]);
|
||||
});
|
||||
|
||||
it('should emit events if updating selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
},
|
||||
],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(emitter.emit).toHaveBeenCalledWith('FETCH_LABEL_SUGGESTIONS');
|
||||
expect(emitter.emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE');
|
||||
});
|
||||
|
||||
it('should ignore updates with older timestamps', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 100,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0].status).toEqual('open');
|
||||
});
|
||||
|
||||
it('should ignore updates with same timestamps', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 100,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0].status).toEqual('open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION_CONTACT', () => {
|
||||
it('should update conversation contact data', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ id: 1, meta: { sender: { id: 1, name: 'Old Name' } } },
|
||||
],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
conversationId: 1,
|
||||
id: 1,
|
||||
name: 'New Name',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION_CONTACT](state, payload);
|
||||
// The mutation extracts all properties except conversationId
|
||||
const { conversationId, ...contact } = payload;
|
||||
expect(state.allConversations[0].meta.sender).toEqual(contact);
|
||||
});
|
||||
|
||||
it('should do nothing if conversation is not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
conversationId: 1,
|
||||
id: 1,
|
||||
name: 'New Name',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION_CONTACT](state, payload);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ACTIVE_INBOX', () => {
|
||||
it('should set current inbox as integer', () => {
|
||||
const state = {
|
||||
currentInbox: null,
|
||||
};
|
||||
|
||||
mutations[types.SET_ACTIVE_INBOX](state, '1');
|
||||
expect(state.currentInbox).toBe(1);
|
||||
});
|
||||
|
||||
it('should set null if no inbox ID provided', () => {
|
||||
const state = {
|
||||
currentInbox: 1,
|
||||
};
|
||||
|
||||
mutations[types.SET_ACTIVE_INBOX](state, null);
|
||||
expect(state.currentInbox).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_CONTACT_CONVERSATIONS', () => {
|
||||
it('should remove all conversations with matching contact ID', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ id: 1, meta: { sender: { id: 1 } } },
|
||||
{ id: 2, meta: { sender: { id: 2 } } },
|
||||
{ id: 3, meta: { sender: { id: 1 } } },
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.CLEAR_CONTACT_CONVERSATIONS](state, 1);
|
||||
expect(state.allConversations).toHaveLength(1);
|
||||
expect(state.allConversations[0].id).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_CONVERSATION', () => {
|
||||
it('should add a new conversation', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const conversation = { id: 1, messages: [] };
|
||||
mutations[types.ADD_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([conversation]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to true', () => {
|
||||
const state = {
|
||||
listLoadingStatus: false,
|
||||
};
|
||||
|
||||
mutations[types.SET_LIST_LOADING_STATUS](state);
|
||||
expect(state.listLoadingStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to false', () => {
|
||||
const state = {
|
||||
listLoadingStatus: true,
|
||||
};
|
||||
|
||||
mutations[types.CLEAR_LIST_LOADING_STATUS](state);
|
||||
expect(state.listLoadingStatus).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CHANGE_CHAT_STATUS_FILTER', () => {
|
||||
it('should update chat status filter', () => {
|
||||
const state = {
|
||||
chatStatusFilter: 'open',
|
||||
};
|
||||
|
||||
mutations[types.CHANGE_CHAT_STATUS_FILTER](state, 'resolved');
|
||||
expect(state.chatStatusFilter).toBe('resolved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_ASSIGNEE', () => {
|
||||
it('should update assignee on conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, meta: { assignee: null } }],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
id: 1,
|
||||
assignee: { id: 1, name: 'Agent' },
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_ASSIGNEE](state, payload);
|
||||
expect(state.allConversations[0].meta.assignee).toEqual(payload.assignee);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION', () => {
|
||||
it('should update the sync conversation message ID', () => {
|
||||
const state = {
|
||||
syncConversationsMessages: {},
|
||||
};
|
||||
|
||||
mutations[types.SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION](state, {
|
||||
conversationId: 1,
|
||||
messageId: 100,
|
||||
});
|
||||
|
||||
expect(state.syncConversationsMessages[1]).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,25 +80,8 @@ class BaseActionCableConnector {
|
||||
}
|
||||
|
||||
onReceived = ({ event, data } = {}) => {
|
||||
const loggableEvents = [
|
||||
'conversation.created',
|
||||
'conversation.status_changed',
|
||||
'team.changed',
|
||||
'assignee.changed',
|
||||
'conversation.contact_changed',
|
||||
'conversation.mentioned',
|
||||
'conversation.read',
|
||||
'conversation.updated',
|
||||
];
|
||||
|
||||
if (this.isAValidEvent(data)) {
|
||||
if (this.events[event] && typeof this.events[event] === 'function') {
|
||||
if (window.newrelic && loggableEvents.includes(event)) {
|
||||
window.newrelic.log(
|
||||
`processing actioncable ${event} with ${JSON.stringify(data, null, 2)} at ${Number(Date.now())}`,
|
||||
{ level: 'DEBUG' }
|
||||
);
|
||||
}
|
||||
this.events[event](data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ class ActionCableBroadcastJob < ApplicationJob
|
||||
include Events::Types
|
||||
|
||||
CONVERSATION_UPDATE_EVENTS = [
|
||||
CONVERSATION_CREATED,
|
||||
CONVERSATION_READ,
|
||||
CONVERSATION_UPDATED,
|
||||
TEAM_CHANGED,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user