Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80ab26ad9 | ||
|
|
e523db12f4 | ||
|
|
6b42ff8d39 | ||
|
|
a88fef2e1d | ||
|
|
ee9f1d7adb | ||
|
|
b711bfd2ca | ||
|
|
42af4b1d01 | ||
|
|
9a7318a9db | ||
|
|
48fa7bf72b | ||
|
|
469e724e3a | ||
|
|
0c101b1f6b | ||
|
|
ad4ec9e93b | ||
|
|
5c560c7628 | ||
|
|
8bacbd2b23 | ||
|
|
8aa37907c0 | ||
|
|
dbb164a37d | ||
|
|
fdcfed2cd7 | ||
|
|
b4f758f644 | ||
|
|
4df58501e3 | ||
|
|
693309202e | ||
|
|
ecd9cf0326 | ||
|
|
81d8d3862d | ||
|
|
c31325e982 | ||
|
|
28452b300d | ||
|
|
6784eb9b3d | ||
|
|
1baf5cbe19 | ||
|
|
c2e6ad6376 | ||
|
|
d908c880d2 | ||
|
|
fcc6e2b8b2 | ||
|
|
6cab741392 | ||
|
|
f3bc2476fc | ||
|
|
3214d06a83 | ||
|
|
8ea21eeefd | ||
|
|
fd28ed8d83 | ||
|
|
b5f5c5c1bc | ||
|
|
4ebfae8b44 | ||
|
|
d0824cc86a | ||
|
|
d2583d32e9 | ||
|
|
462ab5241c | ||
|
|
f984d745e7 | ||
|
|
ca13664ef9 | ||
|
|
304c938260 |
@@ -26,9 +26,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
|
||||
# @portal.custom_domain = parsed_custom_domain
|
||||
process_attached_logo if params[:blob_id].present?
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_record_invalid(e)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
before_action :validate_feature_enabled!
|
||||
before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
|
||||
|
||||
# POST /api/v1/accounts/:account_id/whatsapp/authorization
|
||||
# Handles the embedded signup callback data from the Facebook SDK
|
||||
# Handles both initial authorization and reauthorization
|
||||
# If inbox_id is present in params, it performs reauthorization
|
||||
def create
|
||||
validate_embedded_signup_params!
|
||||
channel = process_embedded_signup
|
||||
@@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
|
||||
def process_embedded_signup
|
||||
service = Whatsapp::EmbeddedSignupService.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
business_id: params[:business_id],
|
||||
waba_id: params[:waba_id],
|
||||
phone_number_id: params[:phone_number_id]
|
||||
params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
service.perform
|
||||
end
|
||||
|
||||
def render_success_response(inbox)
|
||||
def fetch_and_validate_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
validate_reauthorization_required
|
||||
end
|
||||
|
||||
def validate_reauthorization_required
|
||||
return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
|
||||
|
||||
render json: {
|
||||
success: false,
|
||||
message: I18n.t('inbox.reauthorization.not_required')
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def can_upgrade_to_embedded_signup?
|
||||
channel = @inbox.channel
|
||||
return false unless channel.provider == 'whatsapp_cloud'
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def render_success_response(inbox)
|
||||
response = {
|
||||
success: true,
|
||||
id: inbox.id,
|
||||
name: inbox.name,
|
||||
channel_type: 'whatsapp'
|
||||
}
|
||||
response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
|
||||
render json: response
|
||||
end
|
||||
|
||||
def render_error_response(error)
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
class Platform::Api::V1::AccountsController < PlatformController
|
||||
def index
|
||||
@resources = @platform_app.platform_app_permissibles
|
||||
.where(permissible_type: 'Account')
|
||||
.includes(:permissible)
|
||||
.map(&:permissible)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
|
||||
@@ -17,7 +17,12 @@ class SlackUploadsController < ApplicationController
|
||||
end
|
||||
|
||||
def blob_url
|
||||
url_for(@blob.representation(resize_to_fill: [250, nil]))
|
||||
# Only generate representations for images
|
||||
if @blob.content_type.start_with?('image/')
|
||||
url_for(@blob.representation(resize_to_fill: [250, nil]))
|
||||
else
|
||||
url_for(@blob)
|
||||
end
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
|
||||
@@ -30,7 +30,8 @@ class Twilio::CallbackController < ApplicationController
|
||||
:NumMedia,
|
||||
:Latitude,
|
||||
:Longitude,
|
||||
:MessageType
|
||||
:MessageType,
|
||||
:ProfileName
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,13 @@ class NotificationFinder
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@notifications.where(read_at: nil).count
|
||||
if type_included?('read')
|
||||
# If we're including read notifications, filter to unread
|
||||
@notifications.where(read_at: nil).count
|
||||
else
|
||||
# Already filtered to unread notifications, just count
|
||||
@notifications.count
|
||||
end
|
||||
end
|
||||
|
||||
def count
|
||||
@@ -27,7 +33,7 @@ class NotificationFinder
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
filter_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@@ -38,7 +44,7 @@ class NotificationFinder
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def fitler_read_notifications
|
||||
def filter_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
|
||||
@@ -18,12 +18,25 @@ module ReportingEventHelper
|
||||
end
|
||||
|
||||
def last_non_human_activity(conversation)
|
||||
# check if a handoff event already exists
|
||||
handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last
|
||||
# Try to get either a handoff or reopened event first
|
||||
# These will always take precedence over any other activity
|
||||
# Also, any of these events can happen at any time in the course of a conversation lifecycle.
|
||||
# So we pick the latest event
|
||||
event = ReportingEvent.where(
|
||||
conversation_id: conversation.id,
|
||||
name: %w[conversation_bot_handoff conversation_opened]
|
||||
).order(event_end_time: :desc).first
|
||||
|
||||
# if a handoff exists, last non human activity is when the handoff ended,
|
||||
# otherwise it's when the conversation was created
|
||||
handoff_event&.event_end_time || conversation.created_at
|
||||
return event.event_end_time if event&.event_end_time
|
||||
|
||||
# Fallback to bot resolved event
|
||||
# Because this will be closest to the most accurate activity instead of conversation.created_at
|
||||
bot_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_resolved').last
|
||||
|
||||
return bot_event.event_end_time if bot_event&.event_end_time
|
||||
|
||||
# If no events found, return conversation creation time
|
||||
conversation.created_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -137,7 +137,6 @@ export default {
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="flex flex-col w-full h-screen min-h-0"
|
||||
:class="{ 'app-rtl--wrapper': isRTL }"
|
||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
|
||||
@@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient {
|
||||
createEmbeddedSignup(params) {
|
||||
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
|
||||
}
|
||||
|
||||
reauthorizeWhatsApp({ inboxId, ...params }) {
|
||||
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
|
||||
...params,
|
||||
inbox_id: inboxId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WhatsappChannel();
|
||||
|
||||
@@ -76,8 +76,8 @@ const campaignStatus = computed(() => {
|
||||
const inboxName = computed(() => props.inbox?.name || '');
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phone_number: phoneNumber, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, phoneNumber);
|
||||
const { medium, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, medium);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+7
-5
@@ -38,11 +38,13 @@ const handleClose = () => emit('close');
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6"
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] rounded-xl border border-n-weak shadow-md max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||
<div class="p-6 flex flex-col gap-6">
|
||||
<h3 class="text-base font-medium text-n-slate-12 flex-shrink-0">
|
||||
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -98,6 +98,7 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
:src="thumbnail"
|
||||
:size="48"
|
||||
:status="availabilityStatus"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 flex-1">
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ const inbox = computed(() => props.stateInbox);
|
||||
const inboxName = computed(() => inbox.value?.name);
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phoneNumber, channelType } = inbox.value;
|
||||
return getInboxIconByType(channelType, phoneNumber);
|
||||
const { channelType, medium } = inbox.value;
|
||||
return getInboxIconByType(channelType, medium);
|
||||
});
|
||||
|
||||
const lastActivityAt = computed(() => {
|
||||
|
||||
+6
-4
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, helpers } from '@vuelidate/validators';
|
||||
import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
|
||||
import { required, minLength, helpers, url } from '@vuelidate/validators';
|
||||
import { isValidSlug } from 'shared/helpers/Validators';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@@ -71,7 +71,7 @@ const rules = {
|
||||
isValidSlug
|
||||
),
|
||||
},
|
||||
homePageLink: { shouldBeUrl },
|
||||
homePageLink: { url },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
@@ -315,7 +315,9 @@ const handleAvatarDelete = () => {
|
||||
class="[&>div>button:not(.focused)]:!outline-n-weak"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
|
||||
@@ -49,8 +49,8 @@ const isUnread = computed(() => !props.inboxItem?.readAt);
|
||||
const inbox = computed(() => props.stateInbox);
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phoneNumber, channelType } = inbox.value;
|
||||
return getInboxIconByType(channelType, phoneNumber);
|
||||
const { channelType, medium } = inbox.value;
|
||||
return getInboxIconByType(channelType, medium);
|
||||
});
|
||||
|
||||
const hasSlaThreshold = computed(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
processContactableInboxes,
|
||||
mergeInboxDetails,
|
||||
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue';
|
||||
|
||||
@@ -37,9 +39,16 @@ const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
const { fetchSignatureFlagFromUISettings } = useUISettings();
|
||||
|
||||
const isSmallScreen = computed(
|
||||
() => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
|
||||
);
|
||||
|
||||
const viewInModal = computed(() => props.isModal || isSmallScreen.value);
|
||||
|
||||
const contacts = ref([]);
|
||||
const selectedContact = ref(null);
|
||||
const targetInbox = ref(null);
|
||||
@@ -67,7 +76,7 @@ const directUploadsEnabled = computed(
|
||||
const activeContact = computed(() => contactById.value(props.contactId));
|
||||
|
||||
const composePopoverClass = computed(() => {
|
||||
if (props.isModal) return '';
|
||||
if (viewInModal.value) return '';
|
||||
|
||||
return props.alignPosition === 'right'
|
||||
? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
|
||||
@@ -179,14 +188,18 @@ const toggle = () => {
|
||||
|
||||
watch(
|
||||
activeContact,
|
||||
() => {
|
||||
if (activeContact.value && props.contactId) {
|
||||
const contactInboxes = activeContact.value?.contactInboxes || [];
|
||||
(currentContact, previousContact) => {
|
||||
if (currentContact && props.contactId) {
|
||||
// Reset on contact change
|
||||
if (currentContact?.id !== previousContact?.id) clearSelectedContact();
|
||||
|
||||
// First process the contactable inboxes to get the right structure
|
||||
const processedInboxes = processContactableInboxes(contactInboxes);
|
||||
const processedInboxes = processContactableInboxes(
|
||||
currentContact.contactInboxes || []
|
||||
);
|
||||
// Then Merge processedInboxes with the inboxes list
|
||||
selectedContact.value = {
|
||||
...activeContact.value,
|
||||
...currentContact,
|
||||
contactInboxes: mergeInboxDetails(processedInboxes, inboxesList.value),
|
||||
};
|
||||
}
|
||||
@@ -202,7 +215,7 @@ const handleClickOutside = () => {
|
||||
};
|
||||
|
||||
const onModalBackdropClick = () => {
|
||||
if (!props.isModal) return;
|
||||
if (!viewInModal.value) return;
|
||||
handleClickOutside();
|
||||
};
|
||||
|
||||
@@ -231,7 +244,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
]"
|
||||
class="relative"
|
||||
:class="{
|
||||
'z-40': showComposeNewConversation,
|
||||
'z-50': showComposeNewConversation && !viewInModal,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
@@ -243,12 +256,12 @@ useKeyboardEvents(keyboardEvents);
|
||||
v-if="showComposeNewConversation"
|
||||
:class="{
|
||||
'fixed z-50 bg-n-alpha-black1 backdrop-blur-[4px] flex items-start pt-[clamp(3rem,15vh,12rem)] justify-center inset-0':
|
||||
isModal,
|
||||
viewInModal,
|
||||
}"
|
||||
@click.self="onModalBackdropClick"
|
||||
>
|
||||
<ComposeNewConversationForm
|
||||
:class="[{ 'mt-2': !isModal }, composePopoverClass]"
|
||||
:class="[{ 'mt-2': !viewInModal }, composePopoverClass]"
|
||||
:contacts="contacts"
|
||||
:contact-id="contactId"
|
||||
:is-loading="isSearching"
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
/>
|
||||
<EmojiInput
|
||||
v-if="isEmojiPickerOpen"
|
||||
class="left-0 top-full mt-1.5"
|
||||
class="ltr:left-0 rtl:right-0 top-full mt-1.5"
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ const removeAttachment = id => {
|
||||
variant="ghost"
|
||||
icon="i-lucide-trash"
|
||||
color="slate"
|
||||
class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
|
||||
class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
|
||||
@click="removeAttachment(attachment.resource.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -265,7 +265,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl min-w-0"
|
||||
>
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => {
|
||||
<DropdownMenu
|
||||
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
:menu-items="contactableInboxesList"
|
||||
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
@action="emit('handleInboxAction', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+9
-5
@@ -3,8 +3,9 @@ import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
|
||||
import WhatsappTemplate from './WhatsappTemplate.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inboxId: {
|
||||
@@ -73,10 +74,13 @@ const handleSendMessage = template => {
|
||||
/>
|
||||
<div
|
||||
v-if="showTemplatesMenu"
|
||||
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="relative w-full">
|
||||
<span class="absolute top-2 left-3 i-lucide-search size-3.5" />
|
||||
<Icon
|
||||
icon="i-lucide-search"
|
||||
class="absolute size-3.5 top-2 ltr:left-3 rtl:right-3"
|
||||
/>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
@@ -85,7 +89,7 @@ const handleSendMessage = template => {
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
class="py-2 pr-2 pl-10 w-full h-8 text-sm rounded-lg border-none outline-none reset-base bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -105,7 +109,7 @@ const handleSendMessage = template => {
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<WhatsappTemplateParser
|
||||
<WhatsappTemplate
|
||||
v-if="selectedTemplate"
|
||||
:template="selectedTemplate"
|
||||
@send-message="handleSendMessage"
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleSendMessage = payload => {
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="w-full">
|
||||
<WhatsAppTemplateParser
|
||||
:template="template"
|
||||
@send-message="handleSendMessage"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #actions="{ sendMessage, goBack, disabled }">
|
||||
<div class="flex gap-3 justify-between items-end w-full h-14">
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.BACK'
|
||||
)
|
||||
"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="w-full font-medium"
|
||||
@click="goBack"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.SEND_MESSAGE'
|
||||
)
|
||||
"
|
||||
class="w-full font-medium"
|
||||
:disabled="disabled"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</WhatsAppTemplateParser>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+153
-39
@@ -1,12 +1,16 @@
|
||||
<script setup>
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,50 +18,160 @@ const emit = defineEmits(['sendMessage', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleSendMessage = payload => {
|
||||
const processedParams = ref({});
|
||||
|
||||
const templateName = computed(() => {
|
||||
return props.template?.name || '';
|
||||
});
|
||||
|
||||
const templateString = computed(() => {
|
||||
return props.template?.components?.find(
|
||||
component => component.type === 'BODY'
|
||||
).text;
|
||||
});
|
||||
|
||||
const processVariable = str => {
|
||||
return str.replace(/{{|}}/g, '');
|
||||
};
|
||||
|
||||
const processedString = computed(() => {
|
||||
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
const variableKey = processVariable(variable);
|
||||
return processedParams.value[variableKey] || `{{${variable}}}`;
|
||||
});
|
||||
});
|
||||
|
||||
const processedStringWithVariableHighlight = computed(() => {
|
||||
const variables = templateString.value.match(/{{([^}]+)}}/g) || [];
|
||||
|
||||
return variables.reduce((result, variable) => {
|
||||
const variableKey = processVariable(variable);
|
||||
const value = processedParams.value[variableKey] || variable;
|
||||
return result.replace(
|
||||
variable,
|
||||
`<span class="break-all text-n-slate-12">${value}</span>`
|
||||
);
|
||||
}, templateString.value);
|
||||
});
|
||||
|
||||
const rules = computed(() => {
|
||||
const paramRules = {};
|
||||
Object.keys(processedParams.value).forEach(key => {
|
||||
paramRules[key] = { required: requiredIf(true) };
|
||||
});
|
||||
return {
|
||||
processedParams: paramRules,
|
||||
};
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(rules, { processedParams });
|
||||
|
||||
const getFieldErrorType = key => {
|
||||
if (!v$.value.processedParams[key]?.$error) return 'info';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) return;
|
||||
|
||||
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
const isValid = await v$.value.$validate();
|
||||
if (!isValid) return;
|
||||
|
||||
const payload = {
|
||||
message: processedString.value,
|
||||
templateParams: {
|
||||
name: props.template.name,
|
||||
category: props.template.category,
|
||||
language: props.template.language,
|
||||
namespace: props.template.namespace,
|
||||
processed_params: processedParams.value,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
onMounted(() => {
|
||||
generateVariables();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="w-full">
|
||||
<WhatsAppTemplateParser
|
||||
:template="template"
|
||||
@send-message="handleSendMessage"
|
||||
@back="handleBack"
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.TEMPLATE_NAME',
|
||||
{ templateName: templateName }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<p
|
||||
class="mb-0 text-sm text-n-slate-11"
|
||||
v-html="processedStringWithVariableHighlight"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="Object.keys(processedParams).length"
|
||||
class="text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.VARIABLES'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="flex items-center w-full gap-2"
|
||||
>
|
||||
<span
|
||||
class="block h-8 text-sm min-w-6 text-start truncate text-n-slate-10 leading-8"
|
||||
:title="key"
|
||||
>
|
||||
<template #actions="{ sendMessage, goBack }">
|
||||
<div class="flex gap-3 justify-between items-end w-full h-14">
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.BACK'
|
||||
)
|
||||
"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="w-full font-medium"
|
||||
@click="goBack"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.SEND_MESSAGE'
|
||||
)
|
||||
"
|
||||
class="w-full font-medium"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</WhatsAppTemplateParser>
|
||||
{{ key }}
|
||||
</span>
|
||||
<Input
|
||||
v-model="processedParams[key]"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
:message-type="getFieldErrorType(key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between w-full gap-3 h-14">
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.BACK'
|
||||
)
|
||||
"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="w-full font-medium"
|
||||
@click="emit('back')"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.SEND_MESSAGE'
|
||||
)
|
||||
"
|
||||
class="w-full font-medium"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+2
-1
@@ -36,10 +36,11 @@ const transformInbox = ({
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
medium,
|
||||
...rest
|
||||
}) => ({
|
||||
id,
|
||||
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
|
||||
icon: getInboxIconByType(channelType, medium, 'line'),
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,10 +34,18 @@ const props = defineProps({
|
||||
validator: value =>
|
||||
!value || wootConstants.AVAILABILITY_STATUS_KEYS.includes(value),
|
||||
},
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hideOfflineStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['upload', 'delete']);
|
||||
@@ -66,11 +75,11 @@ const AVATAR_COLORS = {
|
||||
default: { bg: '#E8E8E8', text: '#60646C' },
|
||||
};
|
||||
|
||||
const STATUS_CLASSES = {
|
||||
const STATUS_CLASSES = computed(() => ({
|
||||
online: 'bg-n-teal-10',
|
||||
busy: 'bg-n-amber-10',
|
||||
offline: 'bg-n-slate-10',
|
||||
};
|
||||
...(props.hideOfflineStatus ? {} : { offline: 'bg-n-slate-10' }),
|
||||
}));
|
||||
|
||||
const showDefaultAvatar = computed(() => !props.src && !props.name);
|
||||
|
||||
@@ -174,21 +183,31 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="relative inline-flex group/avatar z-0" :style="containerStyles">
|
||||
<span
|
||||
class="relative inline-flex group/avatar z-0 flex-shrink-0"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
<slot name="badge" :size="size">
|
||||
<div
|
||||
v-if="status"
|
||||
v-if="status && STATUS_CLASSES[status]"
|
||||
class="absolute z-20 border rounded-full border-n-slate-3"
|
||||
:style="badgeStyles"
|
||||
:class="STATUS_CLASSES[status]"
|
||||
/>
|
||||
<div
|
||||
v-if="inbox && !(status && STATUS_CLASSES[status])"
|
||||
:style="badgeStyles"
|
||||
class="absolute z-20 flex items-center justify-center rounded-full bg-n-solid-1 border border-transparent flex-shrink-0"
|
||||
>
|
||||
<ChannelIcon :inbox="inbox" class="w-full h-full text-n-slate-11" />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- Delete Avatar Button -->
|
||||
<div
|
||||
v-if="src && allowUpload"
|
||||
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 -right-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 ltr:-right-2 rtl:-left-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleDismiss"
|
||||
>
|
||||
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />
|
||||
@@ -232,31 +251,40 @@ watch(
|
||||
<!-- Fallback Icon if no name or image -->
|
||||
<Icon
|
||||
v-else
|
||||
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
|
||||
:title="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
|
||||
icon="i-lucide-user"
|
||||
:style="iconStyles"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Upload Overlay and Input -->
|
||||
<div
|
||||
v-if="allowUpload"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleUploadAvatar"
|
||||
<slot
|
||||
v-if="allowUpload || $slots.overlay"
|
||||
name="overlay"
|
||||
:size="size"
|
||||
:handle-upload="handleUploadAvatar"
|
||||
:file-input-ref="fileInput"
|
||||
:handle-image-upload="handleImageUpload"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-upload"
|
||||
class="text-white"
|
||||
:style="{ width: `${size / 2}px`, height: `${size / 2}px` }"
|
||||
/>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-upload"
|
||||
class="text-white"
|
||||
:style="{ width: `${size / 2}px`, height: `${size / 2}px` }"
|
||||
/>
|
||||
<input
|
||||
v-if="allowUpload"
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -57,9 +57,10 @@ const menuItems = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const icon = computed(() =>
|
||||
getInboxIconByType(props.inbox.channel_type, '', 'outline')
|
||||
);
|
||||
const icon = computed(() => {
|
||||
const { medium, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, medium, 'outline');
|
||||
});
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
|
||||
+10
@@ -35,6 +35,7 @@ const initialState = {
|
||||
productName: '',
|
||||
featureFaq: false,
|
||||
featureMemory: false,
|
||||
featureCitation: false,
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
@@ -70,6 +71,7 @@ const prepareAssistantDetails = () => ({
|
||||
product_name: state.productName,
|
||||
feature_faq: state.featureFaq,
|
||||
feature_memory: state.featureMemory,
|
||||
feature_citation: state.featureCitation,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,6 +95,7 @@ const updateStateFromAssistant = assistant => {
|
||||
productName: config.product_name,
|
||||
featureFaq: config.feature_faq || false,
|
||||
featureMemory: config.feature_memory || false,
|
||||
featureCitation: config.feature_citation || false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -151,6 +154,13 @@ watch(
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="state.featureCitation" type="checkbox" />
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
|
||||
+8
-6
@@ -41,6 +41,7 @@ const initialState = {
|
||||
features: {
|
||||
conversationFaqs: false,
|
||||
memories: false,
|
||||
citations: false,
|
||||
},
|
||||
temperature: 1,
|
||||
};
|
||||
@@ -87,6 +88,7 @@ const updateStateFromAssistant = assistant => {
|
||||
state.features = {
|
||||
conversationFaqs: config.feature_faq || false,
|
||||
memories: config.feature_memory || false,
|
||||
citations: config.feature_citation || false,
|
||||
};
|
||||
state.temperature = config.temperature || 1;
|
||||
};
|
||||
@@ -152,6 +154,7 @@ const handleFeaturesUpdate = () => {
|
||||
...props.assistant.config,
|
||||
feature_faq: state.features.conversationFaqs,
|
||||
feature_memory: state.features.memories,
|
||||
feature_citation: state.features.citations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -300,20 +303,19 @@ watch(
|
||||
<input
|
||||
v-model="state.features.conversationFaqs"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
{{
|
||||
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
|
||||
}}
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="state.features.memories"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
<input v-model="state.features.memories" type="checkbox" />
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="state.features.citations" type="checkbox" />
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+9
-10
@@ -26,6 +26,7 @@ const initialState = {
|
||||
features: {
|
||||
conversationFaqs: false,
|
||||
memories: false,
|
||||
citations: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -57,6 +58,7 @@ const updateStateFromAssistant = assistant => {
|
||||
state.features = {
|
||||
conversationFaqs: config.feature_faq || false,
|
||||
memories: config.feature_memory || false,
|
||||
citations: config.feature_citation || false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,6 +78,7 @@ const handleBasicInfoUpdate = async () => {
|
||||
product_name: state.productName,
|
||||
feature_faq: state.features.conversationFaqs,
|
||||
feature_memory: state.features.memories,
|
||||
feature_citation: state.features.citations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,21 +126,17 @@ watch(
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="state.features.conversationFaqs"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
<input v-model="state.features.conversationFaqs" type="checkbox" />
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="state.features.memories"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
<input v-model="state.features.memories" type="checkbox" />
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="state.features.citations" type="checkbox" />
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useChannelIcon } from './provider';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
@@ -9,7 +10,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const channelIcon = useChannelIcon(props.inbox);
|
||||
const channelIcon = useChannelIcon(toRef(props, 'inbox'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -22,15 +22,21 @@ export function useChannelIcon(inbox) {
|
||||
};
|
||||
|
||||
const channelIcon = computed(() => {
|
||||
const type = inbox.channel_type;
|
||||
const inboxDetails = inbox.value || inbox;
|
||||
const type = inboxDetails.channel_type;
|
||||
let icon = channelTypeIconMap[type];
|
||||
|
||||
if (type === 'Channel::Email' && inbox.provider) {
|
||||
if (Object.keys(providerIconMap).includes(inbox.provider)) {
|
||||
icon = providerIconMap[inbox.provider];
|
||||
if (type === 'Channel::Email' && inboxDetails.provider) {
|
||||
if (Object.keys(providerIconMap).includes(inboxDetails.provider)) {
|
||||
icon = providerIconMap[inboxDetails.provider];
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for Twilio whatsapp
|
||||
if (type === 'Channel::TwilioSms' && inboxDetails.medium === 'whatsapp') {
|
||||
icon = 'i-ri-whatsapp-fill';
|
||||
}
|
||||
|
||||
return icon ?? 'i-ri-global-fill';
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,77 @@ describe('useChannelIcon', () => {
|
||||
expect(icon).toBe('i-ri-phone-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Line channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Line' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-line-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for SMS channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Sms' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Telegram channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Telegram' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-telegram-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Twitter channel', () => {
|
||||
const inbox = { channel_type: 'Channel::TwitterProfile' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-twitter-x-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for WebWidget channel', () => {
|
||||
const inbox = { channel_type: 'Channel::WebWidget' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-global-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Instagram channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Instagram' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-instagram-fill');
|
||||
});
|
||||
|
||||
describe('TwilioSms channel', () => {
|
||||
it('returns chat icon for regular Twilio SMS channel', () => {
|
||||
const inbox = { channel_type: 'Channel::TwilioSms' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
|
||||
it('returns WhatsApp icon for Twilio SMS with WhatsApp medium', () => {
|
||||
const inbox = {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
medium: 'whatsapp',
|
||||
};
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-whatsapp-fill');
|
||||
});
|
||||
|
||||
it('returns chat icon for Twilio SMS with non-WhatsApp medium', () => {
|
||||
const inbox = {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
medium: 'sms',
|
||||
};
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
|
||||
it('returns chat icon for Twilio SMS with undefined medium', () => {
|
||||
const inbox = {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
medium: undefined,
|
||||
};
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email channel', () => {
|
||||
it('returns mail icon for generic email channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Email' };
|
||||
|
||||
@@ -509,10 +509,8 @@ const menuItems = computed(() => {
|
||||
class="bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak flex flex-col text-sm pb-1 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 transition-transform duration-200 ease-in-out md:static w-[200px] basis-[200px] md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:-translate-x-0"
|
||||
:class="[
|
||||
{
|
||||
'ltr:translate-x-0 rtl:-translate-x-0 shadow-lg md:shadow-none':
|
||||
isMobileSidebarOpen,
|
||||
'ltr:-translate-x-full rtl:translate-x-full md:translate-x-0':
|
||||
!isMobileSidebarOpen,
|
||||
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
||||
'ltr:-translate-x-full rtl:translate-x-full': !isMobileSidebarOpen,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -218,8 +218,8 @@ onMounted(async () => {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.app-rtl--wrapper .sidebar-group-children > .child-item:last-child::after,
|
||||
.app-rtl--wrapper
|
||||
#app[dir='rtl'] .sidebar-group-children > .child-item:last-child::after,
|
||||
#app[dir='rtl']
|
||||
.sidebar-group-children
|
||||
> *:last-child
|
||||
> *:last-child
|
||||
|
||||
@@ -230,7 +230,7 @@ const handleBlur = e => emit('blur', e);
|
||||
v-if="showDropdownMenu"
|
||||
:menu-items="filteredMenuItems"
|
||||
:is-searching="isLoading"
|
||||
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
|
||||
@action="handleDropdownAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -9,12 +9,22 @@ import {
|
||||
buildTemplateParameters,
|
||||
allKeysRequired,
|
||||
replaceTemplateVariables,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_CATEGORY,
|
||||
COMPONENT_TYPES,
|
||||
MEDIA_FORMATS,
|
||||
findComponentByType,
|
||||
} from 'dashboard/helper/templateHelper';
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
validator: value => {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
if (!value.components || !Array.isArray(value.components)) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,43 +35,66 @@ const { t } = useI18n();
|
||||
const processedParams = ref({});
|
||||
|
||||
const languageLabel = computed(() => {
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || 'en'}`;
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || DEFAULT_LANGUAGE}`;
|
||||
});
|
||||
|
||||
const categoryLabel = computed(() => {
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.CATEGORY')}: ${props.template.category || 'UTILITY'}`;
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.CATEGORY')}: ${props.template.category || DEFAULT_CATEGORY}`;
|
||||
});
|
||||
|
||||
const headerComponent = computed(() => {
|
||||
return props.template.components.find(
|
||||
component => component.type === 'HEADER'
|
||||
);
|
||||
return findComponentByType(props.template, COMPONENT_TYPES.HEADER);
|
||||
});
|
||||
|
||||
const bodyComponent = computed(() => {
|
||||
return props.template.components.find(component => component.type === 'BODY');
|
||||
return findComponentByType(props.template, COMPONENT_TYPES.BODY);
|
||||
});
|
||||
|
||||
const bodyText = computed(() => {
|
||||
return bodyComponent.value.text;
|
||||
return bodyComponent.value?.text || '';
|
||||
});
|
||||
|
||||
const hasMediaHeader = computed(() => {
|
||||
return (
|
||||
headerComponent.value &&
|
||||
headerComponent.value.format &&
|
||||
['IMAGE', 'VIDEO', 'DOCUMENT'].includes(headerComponent.value.format)
|
||||
);
|
||||
const hasMediaHeader = computed(() =>
|
||||
MEDIA_FORMATS.includes(headerComponent.value?.format)
|
||||
);
|
||||
|
||||
const formatType = computed(() => {
|
||||
const format = headerComponent.value?.format;
|
||||
return format ? format.charAt(0) + format.slice(1).toLowerCase() : '';
|
||||
});
|
||||
|
||||
const hasVariables = computed(() => {
|
||||
return bodyText.value.match(/{{([^}]+)}}/g);
|
||||
return bodyText.value?.match(/{{([^}]+)}}/g) !== null;
|
||||
});
|
||||
|
||||
const renderedTemplate = computed(() => {
|
||||
return replaceTemplateVariables(bodyText.value, processedParams.value);
|
||||
});
|
||||
|
||||
const isFormInvalid = computed(() => {
|
||||
if (!hasVariables.value && !hasMediaHeader.value) return false;
|
||||
|
||||
if (hasMediaHeader.value && !processedParams.value.header?.media_url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasVariables.value && processedParams.value.body) {
|
||||
const hasEmptyBodyVariable = Object.values(processedParams.value.body).some(
|
||||
value => !value
|
||||
);
|
||||
if (hasEmptyBodyVariable) return true;
|
||||
}
|
||||
|
||||
if (processedParams.value.buttons) {
|
||||
const hasEmptyButtonParameter = processedParams.value.buttons.some(
|
||||
button => !button.parameter
|
||||
);
|
||||
if (hasEmptyButtonParameter) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
processedParams: {
|
||||
@@ -73,17 +106,14 @@ const v$ = useVuelidate(
|
||||
);
|
||||
|
||||
const initializeTemplateParameters = () => {
|
||||
const templateParameters = buildTemplateParameters(
|
||||
processedParams.value = buildTemplateParameters(
|
||||
props.template,
|
||||
hasMediaHeader.value
|
||||
);
|
||||
processedParams.value = templateParameters;
|
||||
};
|
||||
|
||||
const updateMediaUrl = value => {
|
||||
if (!processedParams.value.header) {
|
||||
processedParams.value.header = {};
|
||||
}
|
||||
processedParams.value.header ??= {};
|
||||
processedParams.value.header.media_url = value;
|
||||
};
|
||||
|
||||
@@ -91,16 +121,16 @@ const sendMessage = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
const finalParams = processedParams.value;
|
||||
const { name, category, language, namespace } = props.template;
|
||||
|
||||
const payload = {
|
||||
message: renderedTemplate.value,
|
||||
templateParams: {
|
||||
name: props.template.name,
|
||||
category: props.template.category,
|
||||
language: props.template.language,
|
||||
namespace: props.template.namespace,
|
||||
processed_params: finalParams,
|
||||
name,
|
||||
category,
|
||||
language,
|
||||
namespace,
|
||||
processed_params: processedParams.value,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
@@ -116,6 +146,15 @@ const goBack = () => {
|
||||
|
||||
onMounted(initializeTemplateParameters);
|
||||
|
||||
watch(
|
||||
() => props.template,
|
||||
() => {
|
||||
initializeTemplateParameters();
|
||||
v$.value.$reset();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
processedParams,
|
||||
hasVariables,
|
||||
@@ -143,7 +182,7 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="rounded-md bg-n-alpha-black3">
|
||||
<div class="rounded-md">
|
||||
<div class="text-sm whitespace-pre-wrap text-n-slate-12">
|
||||
{{ renderedTemplate }}
|
||||
</div>
|
||||
@@ -160,14 +199,8 @@ defineExpose({
|
||||
<p class="mb-2.5 text-sm font-semibold">
|
||||
{{
|
||||
$t('WHATSAPP_TEMPLATES.PARSER.MEDIA_HEADER_LABEL', {
|
||||
type:
|
||||
headerComponent.format.charAt(0) +
|
||||
headerComponent.format.slice(1).toLowerCase(),
|
||||
}) ||
|
||||
`${
|
||||
headerComponent.format.charAt(0) +
|
||||
headerComponent.format.slice(1).toLowerCase()
|
||||
} Header`
|
||||
type: formatType,
|
||||
}) || `${formatType} Header`
|
||||
}}
|
||||
</p>
|
||||
<div class="flex items-center mb-2.5">
|
||||
@@ -177,9 +210,7 @@ defineExpose({
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('WHATSAPP_TEMPLATES.PARSER.MEDIA_URL_LABEL', {
|
||||
type:
|
||||
headerComponent.format.charAt(0) +
|
||||
headerComponent.format.slice(1).toLowerCase(),
|
||||
type: formatType,
|
||||
})
|
||||
"
|
||||
@update:model-value="updateMediaUrl"
|
||||
@@ -242,6 +273,7 @@ defineExpose({
|
||||
:reset-template="resetTemplate"
|
||||
:go-back="goBack"
|
||||
:is-valid="!v$.$invalid"
|
||||
:disabled="isFormInvalid"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -98,7 +98,7 @@ const toggleConversationLayout = () => {
|
||||
/>
|
||||
<div
|
||||
id="saveFilterTeleportTarget"
|
||||
class="absolute z-40 mt-2"
|
||||
class="absolute z-50 mt-2"
|
||||
:class="{ 'ltr:right-0 rtl:left-0': isOnExpandedLayout }"
|
||||
/>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ const toggleConversationLayout = () => {
|
||||
/>
|
||||
<div
|
||||
id="conversationFilterTeleportTarget"
|
||||
class="absolute z-40 mt-2"
|
||||
class="absolute z-50 mt-2"
|
||||
:class="{ 'ltr:right-0 rtl:left-0': isOnExpandedLayout }"
|
||||
/>
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ const toggleConversationLayout = () => {
|
||||
/>
|
||||
<div
|
||||
id="conversationFilterTeleportTarget"
|
||||
class="absolute z-40 mt-2"
|
||||
class="absolute z-50 mt-2"
|
||||
:class="{ 'ltr:right-0 rtl:left-0': isOnExpandedLayout }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
|
||||
/* eslint no-plusplus: 0 */
|
||||
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
|
||||
import Code from './Code.vue';
|
||||
import ColorPicker from './widgets/ColorPicker.vue';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
@@ -18,11 +17,9 @@ import Modal from './Modal.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Tabs from './ui/Tabs/Tabs.vue';
|
||||
import TabsItem from './ui/Tabs/TabsItem.vue';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import DatePicker from './ui/DatePicker/DatePicker.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
Code,
|
||||
ColorPicker,
|
||||
ConfirmDeleteModal,
|
||||
@@ -40,7 +37,6 @@ const WootUIKit = {
|
||||
Spinner,
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
DatePicker,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Avatar',
|
||||
props: {
|
||||
username: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||
};
|
||||
},
|
||||
userInitial() {
|
||||
const parts = this.username.split(/[ -]/);
|
||||
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
|
||||
|
||||
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||
initials = initials.replace(/[a-z]+/g, '');
|
||||
}
|
||||
initials = initials.substring(0, 2).toUpperCase();
|
||||
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="avatar-container" :style="style" aria-hidden="true">
|
||||
<slot>{{ userInitial }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.avatar-color {
|
||||
background-image: linear-gradient(to top, #c2e1ff 0%, #d6ebff 100%);
|
||||
}
|
||||
|
||||
.dark-avatar-color {
|
||||
background-image: linear-gradient(to top, #135899 0%, #135899 100%);
|
||||
}
|
||||
}
|
||||
.avatar-container {
|
||||
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-n-blue-text;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +1,19 @@
|
||||
<script>
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
<script setup>
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
defineProps({
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
computed: {
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center text-n-slate-11 text-xs min-w-0">
|
||||
<fluent-icon
|
||||
class="ltr:mr-0.5 rtl:ml-0.5 flex-shrink-0"
|
||||
:icon="computedInboxClass"
|
||||
size="12"
|
||||
<ChannelIcon
|
||||
:inbox="inbox"
|
||||
class="size-3 ltr:mr-0.5 rtl:ml-0.5 flex-shrink-0"
|
||||
/>
|
||||
<span class="truncate">
|
||||
{{ inbox.name }}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Avatar from './Avatar.vue';
|
||||
import Thumbnail from './Thumbnail.vue';
|
||||
|
||||
describe('Thumbnail.vue', () => {
|
||||
it('should render the agent thumbnail if valid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_valid_url.com',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageLoaded: true,
|
||||
imgError: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render the avatar component if invalid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_invalid_url.com',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageLoaded: true,
|
||||
imgError: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('#image').exists()).toBe(false);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should the initial of the name if no image is passed', () => {
|
||||
const wrapper = mount(Avatar, {
|
||||
propsData: {
|
||||
username: 'Angie Rojas',
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('div').text()).toBe('AR');
|
||||
});
|
||||
});
|
||||
@@ -1,224 +0,0 @@
|
||||
<script>
|
||||
/**
|
||||
* Thumbnail Component
|
||||
* Src - source for round image
|
||||
* Size - Size of the thumbnail
|
||||
* Badge - Chat source indication { fb / telegram }
|
||||
* Username - Username for avatar
|
||||
*/
|
||||
import Avatar from './Avatar.vue';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '40px',
|
||||
},
|
||||
badge: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasBorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shouldShowStatusAlways: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageLoaded: false,
|
||||
imgError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userNameWithoutEmoji() {
|
||||
return removeEmoji(this.username);
|
||||
},
|
||||
showStatusIndicator() {
|
||||
if (this.shouldShowStatusAlways) return true;
|
||||
return this.status === 'online' || this.status === 'busy';
|
||||
},
|
||||
avatarSize() {
|
||||
return Number(this.size.replace(/\D+/g, ''));
|
||||
},
|
||||
badgeSrc() {
|
||||
return {
|
||||
instagram_direct_message: 'instagram-dm',
|
||||
facebook: 'messenger',
|
||||
'twitter-tweet': 'twitter-tweet',
|
||||
'twitter-dm': 'twitter-dm',
|
||||
whatsapp: 'whatsapp',
|
||||
sms: 'sms',
|
||||
'Channel::Line': 'line',
|
||||
'Channel::Telegram': 'telegram',
|
||||
'Channel::WebWidget': '',
|
||||
}[this.badge];
|
||||
},
|
||||
badgeStyle() {
|
||||
const size = Math.floor(this.avatarSize / 3);
|
||||
const badgeSize = `${size + 2}px`;
|
||||
const borderRadius = `${size / 2}px`;
|
||||
return { width: badgeSize, height: badgeSize, borderRadius };
|
||||
},
|
||||
statusStyle() {
|
||||
const statusSize = `${this.avatarSize / 4}px`;
|
||||
return { width: statusSize, height: statusSize };
|
||||
},
|
||||
thumbnailClass() {
|
||||
const className = this.hasBorder
|
||||
? 'border border-solid border-white dark:border-n-weak'
|
||||
: '';
|
||||
const variant =
|
||||
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
|
||||
return `user-thumbnail ${className} ${variant}`;
|
||||
},
|
||||
thumbnailBoxClass() {
|
||||
const boxClass = this.variant === 'circle' ? 'is-rounded' : '';
|
||||
return `user-thumbnail-box ${boxClass}`;
|
||||
},
|
||||
shouldShowImage() {
|
||||
if (!this.src) {
|
||||
return false;
|
||||
}
|
||||
if (this.hasImageLoaded) {
|
||||
return !this.imgError;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
src(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onImgError() {
|
||||
this.imgError = true;
|
||||
},
|
||||
onImgLoad() {
|
||||
this.hasImageLoaded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="thumbnailBoxClass"
|
||||
:style="{ height: size, width: size }"
|
||||
:title="title"
|
||||
>
|
||||
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
|
||||
<slot>
|
||||
<img
|
||||
v-show="shouldShowImage"
|
||||
:src="src"
|
||||
draggable="false"
|
||||
:class="thumbnailClass"
|
||||
@load="onImgLoad"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<Avatar
|
||||
v-show="!shouldShowImage"
|
||||
:username="userNameWithoutEmoji"
|
||||
:class="thumbnailClass"
|
||||
:size="avatarSize"
|
||||
/>
|
||||
</slot>
|
||||
<img
|
||||
v-if="badgeSrc"
|
||||
class="source-badge z-20"
|
||||
:style="badgeStyle"
|
||||
:src="`/integrations/channels/badges/${badgeSrc}.png`"
|
||||
alt="Badge"
|
||||
/>
|
||||
<div
|
||||
v-if="showStatusIndicator"
|
||||
class="z-20"
|
||||
:class="`source-badge user-online-status user-online-status--${status}`"
|
||||
:style="statusStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-thumbnail-box {
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.is-rounded {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-thumbnail {
|
||||
border-radius: 50%;
|
||||
&.thumbnail-square {
|
||||
border-radius: 0.5625rem;
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
object-fit: cover;
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
border-radius: 0.1875rem;
|
||||
bottom: -0.125rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@apply bg-n-background p-0.5 size-3;
|
||||
}
|
||||
|
||||
.user-online-status {
|
||||
@apply bottom-0.5 rounded-full;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
.user-online-status--online {
|
||||
@apply bg-n-teal-10;
|
||||
}
|
||||
|
||||
.user-online-status--busy {
|
||||
@apply bg-n-amber-10;
|
||||
}
|
||||
|
||||
.user-online-status--offline {
|
||||
@apply bg-n-slate-10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,78 +1,69 @@
|
||||
<script>
|
||||
import Thumbnail from './Thumbnail.vue';
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
const props = defineProps({
|
||||
usersList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
usersList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '24px',
|
||||
},
|
||||
showMoreThumbnailsCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
moreThumbnailsText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
gap: {
|
||||
type: String,
|
||||
default: 'normal',
|
||||
validator(value) {
|
||||
// The value must match one of these strings
|
||||
return ['normal', '', 'tight'].includes(value);
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 24,
|
||||
},
|
||||
showMoreThumbnailsCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
moreThumbnailsText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
gap: {
|
||||
type: String,
|
||||
default: 'normal',
|
||||
validator(value) {
|
||||
// The value must match one of these strings
|
||||
return ['normal', 'tight'].includes(value);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const gapClass = computed(() => {
|
||||
if (props.gap === 'tight') {
|
||||
return 'ltr:[&:not(:first-child)]:-ml-2 rtl:[&:not(:first-child)]:-mr-2';
|
||||
}
|
||||
return 'ltr:[&:not(:first-child)]:-ml-1 rtl:[&:not(:first-child)]:-mr-1';
|
||||
});
|
||||
|
||||
const moreThumbnailsClass = computed(() => {
|
||||
if (props.gap === 'tight') {
|
||||
return 'ltr:-ml-2 rtl:-mr-2';
|
||||
}
|
||||
return 'ltr:-ml-1 rtl:-mr-1';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overlapping-thumbnails">
|
||||
<Thumbnail
|
||||
<div class="flex">
|
||||
<Avatar
|
||||
v-for="user in usersList"
|
||||
:key="user.id"
|
||||
v-tooltip="user.name"
|
||||
:title="user.name"
|
||||
:src="user.thumbnail"
|
||||
:username="user.name"
|
||||
has-border
|
||||
:name="user.name"
|
||||
:size="size"
|
||||
:class="`overlapping-thumbnail gap-${gap}`"
|
||||
class="[&>span]:outline [&>span]:outline-1 [&>span]:outline-n-background [&>span]:shadow"
|
||||
:class="gapClass"
|
||||
rounded-full
|
||||
/>
|
||||
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
|
||||
<span
|
||||
v-if="showMoreThumbnailsCount"
|
||||
class="text-n-slate-11 bg-n-slate-4 outline outline-1 outline-n-background text-xs font-medium rounded-full px-2 inline-flex items-center shadow relative"
|
||||
:class="moreThumbnailsClass"
|
||||
>
|
||||
{{ moreThumbnailsText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.overlapping-thumbnails {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.overlapping-thumbnail {
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
|
||||
.gap-tight {
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-more-text {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
@apply text-n-slate-11 bg-n-slate-4 border border-n-weak text-xs font-medium rounded-full px-2 ltr:-ml-2 rtl:-mr-2 inline-flex items-center relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
@@ -7,8 +7,8 @@ defineProps({
|
||||
default: () => ({}),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '20px',
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
@@ -19,11 +19,13 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-1.5 text-left">
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
:src="user.thumbnail"
|
||||
:size="size"
|
||||
:username="user.name"
|
||||
:name="user.name"
|
||||
:status="user.availability_status"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<span class="my-0 truncate text-capitalize" :class="textClass">
|
||||
{{ user.name }}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import MessagePreview from './MessagePreview.vue';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
@@ -14,257 +14,227 @@ import PriorityMark from './PriorityMark.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardLabels,
|
||||
InboxName,
|
||||
Thumbnail,
|
||||
ConversationContextMenu,
|
||||
TimeAgo,
|
||||
MessagePreview,
|
||||
PriorityMark,
|
||||
SLACardLabel,
|
||||
ContextMenu,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
chat: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
hideInboxName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideThumbnail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
teamId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
foldersId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
showAssignee: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
conversationType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableContextMenu: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowedContextMenuOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'contextMenuToggle',
|
||||
'assignAgent',
|
||||
'assignLabel',
|
||||
'assignTeam',
|
||||
'markAsUnread',
|
||||
'markAsRead',
|
||||
'assignPriority',
|
||||
'updateConversationStatus',
|
||||
'deleteConversation',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
hovered: false,
|
||||
showContextMenu: false,
|
||||
contextMenu: {
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
chatMetadata() {
|
||||
return this.chat.meta || {};
|
||||
},
|
||||
const props = defineProps({
|
||||
activeLabel: { type: String, default: '' },
|
||||
chat: { type: Object, default: () => ({}) },
|
||||
hideInboxName: { type: Boolean, default: false },
|
||||
hideThumbnail: { type: Boolean, default: false },
|
||||
teamId: { type: [String, Number], default: 0 },
|
||||
foldersId: { type: [String, Number], default: 0 },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
conversationType: { type: String, default: '' },
|
||||
selected: { type: Boolean, default: false },
|
||||
compact: { type: Boolean, default: false },
|
||||
enableContextMenu: { type: Boolean, default: false },
|
||||
allowedContextMenuOptions: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
assignee() {
|
||||
return this.chatMetadata.assignee || {};
|
||||
},
|
||||
const emit = defineEmits([
|
||||
'contextMenuToggle',
|
||||
'assignAgent',
|
||||
'assignLabel',
|
||||
'assignTeam',
|
||||
'markAsUnread',
|
||||
'markAsRead',
|
||||
'assignPriority',
|
||||
'updateConversationStatus',
|
||||
'deleteConversation',
|
||||
'selectConversation',
|
||||
'deSelectConversation',
|
||||
]);
|
||||
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
this.chatMetadata.sender.id
|
||||
);
|
||||
},
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
isActiveChat() {
|
||||
return this.currentChat.id === this.chat.id;
|
||||
},
|
||||
const hovered = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
const contextMenu = ref({
|
||||
x: null,
|
||||
y: null,
|
||||
});
|
||||
|
||||
unreadCount() {
|
||||
return this.chat.unread_count;
|
||||
},
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const inboxesList = useMapGetter('inboxes/getInboxes');
|
||||
const activeInbox = useMapGetter('getSelectedInbox');
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
hasUnread() {
|
||||
return this.unreadCount > 0;
|
||||
},
|
||||
const chatMetadata = computed(() => props.chat.meta || {});
|
||||
|
||||
isInboxNameVisible() {
|
||||
return !this.activeInbox;
|
||||
},
|
||||
const assignee = computed(() => chatMetadata.value.assignee || {});
|
||||
|
||||
lastMessageInChat() {
|
||||
return getLastMessage(this.chat);
|
||||
},
|
||||
const senderId = computed(() => chatMetadata.value.sender?.id);
|
||||
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
return stateInbox;
|
||||
},
|
||||
const currentContact = computed(() => {
|
||||
return senderId.value
|
||||
? store.getters['contacts/getContact'](senderId.value)
|
||||
: {};
|
||||
});
|
||||
|
||||
showInboxName() {
|
||||
return (
|
||||
!this.hideInboxName &&
|
||||
this.isInboxNameVisible &&
|
||||
this.inboxesList.length > 1
|
||||
);
|
||||
},
|
||||
inboxName() {
|
||||
const stateInbox = this.inbox;
|
||||
return stateInbox.name || '';
|
||||
},
|
||||
hasSlaPolicyId() {
|
||||
return this.chat?.sla_policy_id;
|
||||
},
|
||||
conversationPath() {
|
||||
const { activeInbox, chat } = this;
|
||||
return frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
activeInbox,
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCardClick(e) {
|
||||
const path = this.conversationPath;
|
||||
if (!path) return;
|
||||
const isActiveChat = computed(() => {
|
||||
return currentChat.value.id === props.chat.id;
|
||||
});
|
||||
|
||||
// Handle Ctrl/Cmd + Click for new tab
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
window.open(
|
||||
`${window.chatwootConfig.hostURL}${path}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const unreadCount = computed(() => props.chat.unread_count);
|
||||
|
||||
// Skip if already active
|
||||
if (this.isActiveChat) return;
|
||||
const hasUnread = computed(() => unreadCount.value > 0);
|
||||
|
||||
router.push({ path });
|
||||
},
|
||||
onThumbnailHover() {
|
||||
this.hovered = !this.hideThumbnail;
|
||||
},
|
||||
onThumbnailLeave() {
|
||||
this.hovered = false;
|
||||
},
|
||||
onSelectConversation(checked) {
|
||||
const action = checked ? 'selectConversation' : 'deSelectConversation';
|
||||
this.$emit(action, this.chat.id, this.inbox.id);
|
||||
},
|
||||
openContextMenu(e) {
|
||||
if (!this.enableContextMenu) return;
|
||||
e.preventDefault();
|
||||
this.$emit('contextMenuToggle', true);
|
||||
this.contextMenu.x = e.pageX || e.clientX;
|
||||
this.contextMenu.y = e.pageY || e.clientY;
|
||||
this.showContextMenu = true;
|
||||
},
|
||||
closeContextMenu() {
|
||||
this.$emit('contextMenuToggle', false);
|
||||
this.showContextMenu = false;
|
||||
this.contextMenu.x = null;
|
||||
this.contextMenu.y = null;
|
||||
},
|
||||
onUpdateConversation(status, snoozedUntil) {
|
||||
this.closeContextMenu();
|
||||
this.$emit(
|
||||
'updateConversationStatus',
|
||||
this.chat.id,
|
||||
status,
|
||||
snoozedUntil
|
||||
);
|
||||
},
|
||||
async onAssignAgent(agent) {
|
||||
this.$emit('assignAgent', agent, [this.chat.id]);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async onAssignLabel(label) {
|
||||
this.$emit('assignLabel', [label.title], [this.chat.id]);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async onAssignTeam(team) {
|
||||
this.$emit('assignTeam', team, this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async markAsUnread() {
|
||||
this.$emit('markAsUnread', this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async markAsRead() {
|
||||
this.$emit('markAsRead', this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async assignPriority(priority) {
|
||||
this.$emit('assignPriority', priority, this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async deleteConversation() {
|
||||
this.$emit('deleteConversation', this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
},
|
||||
const isInboxNameVisible = computed(() => !activeInbox.value);
|
||||
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
|
||||
const inboxId = computed(() => props.chat.inbox_id);
|
||||
|
||||
const inbox = computed(() => {
|
||||
return inboxId.value ? store.getters['inboxes/getInbox'](inboxId.value) : {};
|
||||
});
|
||||
|
||||
const showInboxName = computed(() => {
|
||||
return (
|
||||
!props.hideInboxName &&
|
||||
isInboxNameVisible.value &&
|
||||
inboxesList.value.length > 1
|
||||
);
|
||||
});
|
||||
|
||||
const showMetaSection = computed(() => {
|
||||
return (
|
||||
showInboxName.value ||
|
||||
(props.showAssignee && assignee.value.name) ||
|
||||
props.chat.priority
|
||||
);
|
||||
});
|
||||
|
||||
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||
|
||||
const showLabelsSection = computed(() => {
|
||||
return props.chat.labels?.length > 0 || hasSlaPolicyId.value;
|
||||
});
|
||||
|
||||
const messagePreviewClass = computed(() => {
|
||||
return [
|
||||
hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11',
|
||||
!props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '',
|
||||
props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '',
|
||||
];
|
||||
});
|
||||
|
||||
const conversationPath = computed(() => {
|
||||
return frontendURL(
|
||||
conversationUrl({
|
||||
accountId: accountId.value,
|
||||
activeInbox: activeInbox.value,
|
||||
id: props.chat.id,
|
||||
label: props.activeLabel,
|
||||
teamId: props.teamId,
|
||||
conversationType: props.conversationType,
|
||||
foldersId: props.foldersId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const onCardClick = e => {
|
||||
const path = conversationPath.value;
|
||||
if (!path) return;
|
||||
|
||||
// Handle Ctrl/Cmd + Click for new tab
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
window.open(
|
||||
`${window.chatwootConfig.hostURL}${path}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already active
|
||||
if (isActiveChat.value) return;
|
||||
|
||||
router.push({ path });
|
||||
};
|
||||
|
||||
const onThumbnailHover = () => {
|
||||
hovered.value = !props.hideThumbnail;
|
||||
};
|
||||
|
||||
const onThumbnailLeave = () => {
|
||||
hovered.value = false;
|
||||
};
|
||||
|
||||
const onSelectConversation = checked => {
|
||||
if (checked) {
|
||||
emit('selectConversation', props.chat.id, inbox.value.id);
|
||||
} else {
|
||||
emit('deSelectConversation', props.chat.id, inbox.value.id);
|
||||
}
|
||||
};
|
||||
|
||||
const openContextMenu = e => {
|
||||
if (!props.enableContextMenu) return;
|
||||
e.preventDefault();
|
||||
emit('contextMenuToggle', true);
|
||||
contextMenu.value.x = e.pageX || e.clientX;
|
||||
contextMenu.value.y = e.pageY || e.clientY;
|
||||
showContextMenu.value = true;
|
||||
};
|
||||
|
||||
const closeContextMenu = () => {
|
||||
emit('contextMenuToggle', false);
|
||||
showContextMenu.value = false;
|
||||
contextMenu.value.x = null;
|
||||
contextMenu.value.y = null;
|
||||
};
|
||||
|
||||
const onUpdateConversation = (status, snoozedUntil) => {
|
||||
closeContextMenu();
|
||||
emit('updateConversationStatus', props.chat.id, status, snoozedUntil);
|
||||
};
|
||||
|
||||
const onAssignAgent = agent => {
|
||||
emit('assignAgent', agent, [props.chat.id]);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const onAssignLabel = label => {
|
||||
emit('assignLabel', [label.title], [props.chat.id]);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const onAssignTeam = team => {
|
||||
emit('assignTeam', team, props.chat.id);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const markAsUnread = () => {
|
||||
emit('markAsUnread', props.chat.id);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const markAsRead = () => {
|
||||
emit('markAsRead', props.chat.id);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const assignPriority = priority => {
|
||||
emit('assignPriority', priority, props.chat.id);
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
const deleteConversation = () => {
|
||||
emit('deleteConversation', props.chat.id);
|
||||
closeContextMenu();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-3 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 border-t-0 border-b-0 border-l-0 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak':
|
||||
isActiveChat,
|
||||
'unread-chat': hasUnread,
|
||||
'has-inbox-name': showInboxName,
|
||||
'conversation-selected': selected,
|
||||
'bg-n-slate-2 dark:bg-n-slate-3': selected,
|
||||
'px-0': compact,
|
||||
'px-3': !compact,
|
||||
}"
|
||||
@click="onCardClick"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
@@ -274,38 +244,53 @@ export default {
|
||||
@mouseenter="onThumbnailHover"
|
||||
@mouseleave="onThumbnailLeave"
|
||||
>
|
||||
<label
|
||||
v-if="hovered || selected"
|
||||
class="checkbox-wrapper absolute inset-0 z-20 backdrop-blur-[2px]"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:value="selected"
|
||||
:checked="selected"
|
||||
class="checkbox"
|
||||
type="checkbox"
|
||||
@change="onSelectConversation($event.target.checked)"
|
||||
/>
|
||||
</label>
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
v-if="!hideThumbnail"
|
||||
:name="currentContact.name"
|
||||
:src="currentContact.thumbnail"
|
||||
:badge="inboxBadge"
|
||||
:username="currentContact.name"
|
||||
:size="32"
|
||||
:status="currentContact.availability_status"
|
||||
size="32px"
|
||||
/>
|
||||
:inbox="inbox"
|
||||
:class="!showInboxName ? 'mt-4' : 'mt-8'"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
>
|
||||
<template #overlay="{ size }">
|
||||
<label
|
||||
v-if="hovered || selected"
|
||||
class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px]"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:value="selected"
|
||||
:checked="selected"
|
||||
class="!m-0 cursor-pointer"
|
||||
type="checkbox"
|
||||
@change="onSelectConversation($event.target.checked)"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div
|
||||
class="px-0 py-3 border-b group-hover:border-transparent flex-1 border-n-slate-3 w-[calc(100%-40px)]"
|
||||
class="px-0 py-3 border-b group-hover:border-transparent flex-1 border-n-slate-3 min-w-0"
|
||||
>
|
||||
<div class="flex items-center conversation-card--meta min-w-0">
|
||||
<InboxName
|
||||
v-if="showInboxName"
|
||||
:inbox="inbox"
|
||||
class="flex-1 min-w-0 mx-2"
|
||||
/>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div
|
||||
v-if="showMetaSection"
|
||||
class="flex items-center min-w-0 gap-1"
|
||||
:class="{
|
||||
'ltr:ml-2 rtl:mr-2': !compact,
|
||||
'mx-2': compact,
|
||||
}"
|
||||
>
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="flex-1 min-w-0" />
|
||||
<div
|
||||
class="flex items-center gap-2 flex-shrink-0"
|
||||
:class="{
|
||||
'flex-1 justify-between': !showInboxName,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="text-n-slate-11 text-xs font-medium leading-3 py-0.5 px-0 inline-flex items-center truncate"
|
||||
@@ -317,7 +302,7 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<h4
|
||||
class="conversation--user text-sm my-0 mx-2 capitalize pt-0.5 text-ellipsis overflow-hidden whitespace-nowrap w-[calc(100%-70px)] text-n-slate-12"
|
||||
class="conversation--user text-sm my-0 mx-2 capitalize pt-0.5 text-ellipsis overflow-hidden whitespace-nowrap flex-1 min-w-0 ltr:pr-16 rtl:pl-16 text-n-slate-12"
|
||||
:class="hasUnread ? 'font-semibold' : 'font-medium'"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
@@ -325,24 +310,27 @@ export default {
|
||||
<MessagePreview
|
||||
v-if="lastMessageInChat"
|
||||
:message="lastMessageInChat"
|
||||
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm"
|
||||
:class="hasUnread ? 'font-medium text-n-slate-12' : 'text-n-slate-11'"
|
||||
class="my-0 mx-2 leading-6 h-6 flex-1 min-w-0 text-sm"
|
||||
:class="messagePreviewClass"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="conversation--message text-n-slate-11 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="hasUnread ? 'font-medium text-n-slate-12' : 'text-n-slate-11'"
|
||||
class="text-n-slate-11 text-sm my-0 mx-2 leading-6 h-6 flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="messagePreviewClass"
|
||||
>
|
||||
<fluent-icon
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-n-slate-10"
|
||||
icon="info"
|
||||
/>
|
||||
<span>
|
||||
<span class="mx-0.5">
|
||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="absolute flex flex-col mt-4 ltr:right-4 rtl:left-4 top-4">
|
||||
<div
|
||||
class="absolute flex flex-col ltr:right-3 rtl:left-3"
|
||||
:class="showMetaSection ? 'top-8' : 'top-4'"
|
||||
>
|
||||
<span class="ml-auto font-normal leading-4 text-xxs">
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
@@ -350,12 +338,17 @@ export default {
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
||||
class="shadow-lg rounded-full text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
||||
:class="hasUnread ? 'block' : 'hidden'"
|
||||
>
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
</div>
|
||||
<CardLabels :conversation-labels="chat.labels" class="mt-0.5 mx-2 mb-0">
|
||||
<CardLabels
|
||||
v-if="showLabelsSection"
|
||||
:conversation-labels="chat.labels"
|
||||
class="mt-0.5 mx-2 mb-0"
|
||||
>
|
||||
<template v-if="hasSlaPolicyId" #before>
|
||||
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||
</template>
|
||||
@@ -388,55 +381,3 @@ export default {
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.conversation {
|
||||
&.unread-chat {
|
||||
.unread {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
@apply pl-0;
|
||||
|
||||
.conversation-card--meta {
|
||||
@apply ltr:pr-4 rtl:pl-4;
|
||||
}
|
||||
|
||||
.conversation--details {
|
||||
@apply rounded-sm ml-0 pl-5 pr-2;
|
||||
}
|
||||
}
|
||||
|
||||
&::v-deep .user-thumbnail-box {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
&.conversation-selected {
|
||||
@apply bg-n-slate-2 dark:bg-n-slate-3;
|
||||
}
|
||||
|
||||
&.has-inbox-name {
|
||||
&::v-deep .user-thumbnail-box {
|
||||
@apply mt-8;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@apply mt-8;
|
||||
}
|
||||
|
||||
.conversation--meta {
|
||||
@apply mt-4;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@apply flex items-center justify-center rounded-full cursor-pointer mt-4;
|
||||
|
||||
input[type='checkbox'] {
|
||||
@apply m-0 cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useElementSize } from '@vueuse/core';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import MoreActions from './MoreActions.vue';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
@@ -105,12 +105,13 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||
:back-url="backButtonUrl"
|
||||
class="ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
:name="currentContact.name"
|
||||
:src="currentContact.thumbnail"
|
||||
:username="currentContact.name"
|
||||
:size="32"
|
||||
:status="currentContact.availability_status"
|
||||
size="32px"
|
||||
class="flex-shrink-0"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2"
|
||||
|
||||
+2
-1
@@ -36,7 +36,7 @@ const handleResetTemplate = () => {
|
||||
@send-message="handleSendMessage"
|
||||
@reset-template="handleResetTemplate"
|
||||
>
|
||||
<template #actions="{ sendMessage, resetTemplate }">
|
||||
<template #actions="{ sendMessage, resetTemplate, disabled }">
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<NextButton
|
||||
faded
|
||||
@@ -48,6 +48,7 @@ const handleResetTemplate = () => {
|
||||
<NextButton
|
||||
type="button"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
|
||||
:disabled="disabled"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</footer>
|
||||
|
||||
+78
-81
@@ -1,75 +1,71 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, toRef } from 'vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useFunctionGetter, useStore } from 'dashboard/composables/store';
|
||||
import {
|
||||
COMPONENT_TYPES,
|
||||
MEDIA_FORMATS,
|
||||
findComponentByType,
|
||||
} from 'dashboard/helper/templateHelper';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['onSelect'],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
isRefreshing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
whatsAppTemplateMessages() {
|
||||
return this.$store.getters['inboxes/getFilteredWhatsAppTemplates'](
|
||||
this.inboxId
|
||||
);
|
||||
},
|
||||
filteredTemplateMessages() {
|
||||
return this.whatsAppTemplateMessages.filter(template =>
|
||||
template.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getTemplateBody(template) {
|
||||
return (
|
||||
template.components.find(component => component.type === 'BODY')
|
||||
?.text || ''
|
||||
);
|
||||
},
|
||||
getTemplateHeader(template) {
|
||||
return template.components.find(component => component.type === 'HEADER');
|
||||
},
|
||||
getTemplateFooter(template) {
|
||||
return template.components.find(component => component.type === 'FOOTER');
|
||||
},
|
||||
getTemplateButtons(template) {
|
||||
return template.components.find(
|
||||
component => component.type === 'BUTTONS'
|
||||
);
|
||||
},
|
||||
hasMediaContent(template) {
|
||||
const header = this.getTemplateHeader(template);
|
||||
return (
|
||||
header &&
|
||||
(header.format === 'IMAGE' ||
|
||||
header.format === 'VIDEO' ||
|
||||
header.format === 'DOCUMENT')
|
||||
);
|
||||
},
|
||||
async refreshTemplates() {
|
||||
this.isRefreshing = true;
|
||||
try {
|
||||
await this.$store.dispatch('inboxes/syncTemplates', this.inboxId);
|
||||
useAlert(this.$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_ERROR'));
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
},
|
||||
const props = defineProps({
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onSelect']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const query = ref('');
|
||||
const isRefreshing = ref(false);
|
||||
|
||||
const whatsAppTemplateMessages = useFunctionGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates',
|
||||
toRef(props, 'inboxId')
|
||||
);
|
||||
|
||||
const filteredTemplateMessages = computed(() =>
|
||||
whatsAppTemplateMessages.value.filter(template =>
|
||||
template.name.toLowerCase().includes(query.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const getTemplateBody = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.BODY)?.text || '';
|
||||
};
|
||||
|
||||
const getTemplateHeader = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.HEADER);
|
||||
};
|
||||
|
||||
const getTemplateFooter = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.FOOTER);
|
||||
};
|
||||
|
||||
const getTemplateButtons = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.BUTTONS);
|
||||
};
|
||||
|
||||
const hasMediaContent = template => {
|
||||
const header = getTemplateHeader(template);
|
||||
return header && MEDIA_FORMATS.includes(header.format);
|
||||
};
|
||||
|
||||
const refreshTemplates = async () => {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
await store.dispatch('inboxes/syncTemplates', props.inboxId);
|
||||
useAlert(t('WHATSAPP_TEMPLATES.PICKER.REFRESH_SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('WHATSAPP_TEMPLATES.PICKER.REFRESH_ERROR'));
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -83,14 +79,14 @@ export default {
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||
:placeholder="t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||
class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
:disabled="isRefreshing"
|
||||
class="flex justify-center items-center w-9 h-9 rounded-lg bg-n-alpha-black2 outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 hover:bg-n-alpha-2 dark:hover:bg-n-solid-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:title="$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_BUTTON')"
|
||||
:title="t('WHATSAPP_TEMPLATES.PICKER.REFRESH_BUTTON')"
|
||||
@click="refreshTemplates"
|
||||
>
|
||||
<Icon
|
||||
@@ -106,7 +102,7 @@ export default {
|
||||
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||
<button
|
||||
class="block p-2.5 w-full text-left rounded-lg cursor-pointer hover:bg-n-alpha-2 dark:hover:bg-n-solid-2"
|
||||
@click="$emit('onSelect', template)"
|
||||
@click="emit('onSelect', template)"
|
||||
>
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2.5">
|
||||
@@ -116,14 +112,14 @@ export default {
|
||||
<span
|
||||
class="inline-block px-2 py-1 text-xs leading-none rounded-lg cursor-default bg-n-slate-3 text-n-slate-12"
|
||||
>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }}:
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }}:
|
||||
{{ template.language }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div v-if="getTemplateHeader(template)" class="mb-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.HEADER') || 'HEADER' }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.HEADER') || 'HEADER' }}
|
||||
</p>
|
||||
<div
|
||||
v-if="getTemplateHeader(template).format === 'TEXT'"
|
||||
@@ -136,9 +132,10 @@ export default {
|
||||
class="text-sm italic text-n-slate-11"
|
||||
>
|
||||
{{
|
||||
$t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT', {
|
||||
t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT', {
|
||||
format: getTemplateHeader(template).format,
|
||||
}) || `${getTemplateHeader(template).format} media content`
|
||||
}) ||
|
||||
`${getTemplateHeader(template).format} ${t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT_FALLBACK')}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +143,7 @@ export default {
|
||||
<!-- Body -->
|
||||
<div>
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.BODY') || 'BODY' }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.BODY') || 'BODY' }}
|
||||
</p>
|
||||
<p class="text-sm label-body">{{ getTemplateBody(template) }}</p>
|
||||
</div>
|
||||
@@ -154,7 +151,7 @@ export default {
|
||||
<!-- Footer -->
|
||||
<div v-if="getTemplateFooter(template)" class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.FOOTER') || 'FOOTER' }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.FOOTER') || 'FOOTER' }}
|
||||
</p>
|
||||
<p class="text-sm label-body">
|
||||
{{ getTemplateFooter(template).text }}
|
||||
@@ -164,7 +161,7 @@ export default {
|
||||
<!-- Buttons -->
|
||||
<div v-if="getTemplateButtons(template)" class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.BUTTONS') || 'BUTTONS' }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.BUTTONS') || 'BUTTONS' }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<span
|
||||
@@ -179,7 +176,7 @@ export default {
|
||||
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.CATEGORY') || 'CATEGORY' }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.CATEGORY') || 'CATEGORY' }}
|
||||
</p>
|
||||
<p class="text-sm">{{ template.category }}</p>
|
||||
</div>
|
||||
@@ -194,13 +191,13 @@ export default {
|
||||
<div v-if="!filteredTemplateMessages.length" class="py-8 text-center">
|
||||
<div v-if="query && whatsAppTemplateMessages.length">
|
||||
<p>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||
<strong>{{ query }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="!whatsAppTemplateMessages.length" class="space-y-4">
|
||||
<p class="text-n-slate-11">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { downloadFile } from '@chatwoot/utils';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -186,10 +186,12 @@ onMounted(() => {
|
||||
v-if="senderDetails"
|
||||
class="flex items-center min-w-[15rem] shrink-0"
|
||||
>
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
v-if="senderDetails.avatar"
|
||||
:username="senderDetails.name"
|
||||
:name="senderDetails.name"
|
||||
:src="senderDetails.avatar"
|
||||
:size="40"
|
||||
rounded-full
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
|
||||
|
||||
+6
-13
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
// components
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from '../../Avatar.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
// composables
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
@@ -226,18 +226,16 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="sender--info has-tooltip" data-original-title="null">
|
||||
<woot-thumbnail
|
||||
<Avatar
|
||||
v-tooltip.top="{
|
||||
content: $t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'),
|
||||
delay: { show: 600, hide: 0 },
|
||||
hideOnClick: true,
|
||||
}"
|
||||
size="16px"
|
||||
>
|
||||
<Avatar class="user-thumbnail thumbnail-rounded">
|
||||
<fluent-icon class="chatwoot-ai-icon" icon="chatwoot-ai" />
|
||||
</Avatar>
|
||||
</woot-thumbnail>
|
||||
:size="16"
|
||||
name="chatwoot-ai"
|
||||
icon-name="i-lucide-sparkles"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -268,11 +266,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.chatwoot-ai-icon {
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.label-suggestion--title {
|
||||
@apply text-n-slate-11 mt-0.5 text-xxs;
|
||||
}
|
||||
|
||||
+7
-5
@@ -1,12 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Avatar,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
@@ -123,11 +123,13 @@ export default {
|
||||
</li>
|
||||
<li v-for="agent in filteredAgents" :key="agent.id">
|
||||
<div class="agent-list-item" @click="assignAgent(agent)">
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
:name="agent.name"
|
||||
:src="agent.thumbnail"
|
||||
:status="agent.availability_status"
|
||||
:username="agent.name"
|
||||
size="22px"
|
||||
:size="22"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<span class="my-0 text-n-slate-12">
|
||||
{{ agent.name }}
|
||||
|
||||
+1
-1
@@ -263,7 +263,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bulk-action__container {
|
||||
@apply p-4 relative border-b border-solid border-n-strong dark:border-n-weak;
|
||||
@apply p-3 relative border-b border-solid border-n-strong dark:border-n-weak;
|
||||
}
|
||||
|
||||
.bulk-action__panel {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<script>
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
usernameAvatar: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
deleteAvatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['onAvatarSelect', 'onAvatarDelete'],
|
||||
watch: {},
|
||||
methods: {
|
||||
handleImageUpload(event) {
|
||||
const [file] = event.target.files;
|
||||
|
||||
this.$emit('onAvatarSelect', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
},
|
||||
onAvatarDelete() {
|
||||
this.$refs.file.value = null;
|
||||
this.$emit('onAvatarDelete');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</label>
|
||||
<woot-thumbnail
|
||||
v-if="src"
|
||||
size="80px"
|
||||
:src="src"
|
||||
:username="usernameAvatar"
|
||||
/>
|
||||
<div v-if="src && deleteAvatar" class="my-1">
|
||||
<NextButton
|
||||
outline
|
||||
xs
|
||||
ruby
|
||||
:label="$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT')"
|
||||
@click="onAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
<label>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
@@ -196,6 +196,7 @@ describe('useAutomation', () => {
|
||||
automationTypes.conversation_created = { conditions: [] };
|
||||
automationTypes.conversation_updated = { conditions: [] };
|
||||
automationTypes.conversation_opened = { conditions: [] };
|
||||
automationTypes.conversation_resolved = { conditions: [] };
|
||||
|
||||
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
|
||||
automationHelper.generateCustomAttributes.mockReturnValue([]);
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('useMacros', () => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'labels/getLabels': { value: mockLabels },
|
||||
'teams/getTeams': { value: mockTeams },
|
||||
'agents/getAgents': { value: mockAgents },
|
||||
'agents/getVerifiedAgents': { value: mockAgents },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,24 +119,30 @@ describe('useMacros', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length);
|
||||
expect(getMacroDropdownValues('assign_team')).toHaveLength(
|
||||
mockTeams.length
|
||||
);
|
||||
mockTeams.length + 1
|
||||
); // +1 for "None"
|
||||
expect(getMacroDropdownValues('assign_agent')).toHaveLength(
|
||||
mockAgents.length + 1
|
||||
); // +1 for "Self"
|
||||
mockAgents.length + 2
|
||||
); // +2 for "None" and "Self"
|
||||
});
|
||||
|
||||
it('returns teams for assign_team and send_email_to_team types', () => {
|
||||
it('returns teams with "None" option for assign_team and teams only for send_email_to_team', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams);
|
||||
const assignTeamResult = getMacroDropdownValues('assign_team');
|
||||
expect(assignTeamResult[0]).toEqual({
|
||||
id: 'nil',
|
||||
name: 'AUTOMATION.NONE_OPTION',
|
||||
});
|
||||
expect(assignTeamResult.slice(1)).toEqual(mockTeams);
|
||||
expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams);
|
||||
});
|
||||
|
||||
it('returns agents with "Self" option for assign_agent type', () => {
|
||||
it('returns agents with "None" and "Self" options for assign_agent type', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
const result = getMacroDropdownValues('assign_agent');
|
||||
expect(result[0]).toEqual({ id: 'self', name: 'Self' });
|
||||
expect(result.slice(1)).toEqual(mockAgents);
|
||||
expect(result[0]).toEqual({ id: 'nil', name: 'AUTOMATION.NONE_OPTION' });
|
||||
expect(result[1]).toEqual({ id: 'self', name: 'Self' });
|
||||
expect(result.slice(2)).toEqual(mockAgents);
|
||||
});
|
||||
|
||||
it('returns formatted labels for add_label and remove_label types', () => {
|
||||
@@ -167,13 +173,16 @@ describe('useMacros', () => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'labels/getLabels': { value: [] },
|
||||
'teams/getTeams': { value: [] },
|
||||
'agents/getAgents': { value: [] },
|
||||
'agents/getVerifiedAgents': { value: [] },
|
||||
});
|
||||
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('add_label')).toEqual([]);
|
||||
expect(getMacroDropdownValues('assign_team')).toEqual([]);
|
||||
expect(getMacroDropdownValues('assign_team')).toEqual([
|
||||
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
|
||||
]);
|
||||
expect(getMacroDropdownValues('assign_agent')).toEqual([
|
||||
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
|
||||
{ id: 'self', name: 'Self' },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,12 @@ export const useMacros = () => {
|
||||
|
||||
const labels = computed(() => getters['labels/getLabels'].value);
|
||||
const teams = computed(() => getters['teams/getTeams'].value);
|
||||
const agents = computed(() => getters['agents/getAgents'].value);
|
||||
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
|
||||
|
||||
const withNoneOption = options => [
|
||||
{ id: 'nil', name: t('AUTOMATION.NONE_OPTION') },
|
||||
...(options || []),
|
||||
];
|
||||
|
||||
/**
|
||||
* Get dropdown values based on the specified type
|
||||
@@ -23,10 +28,15 @@ export const useMacros = () => {
|
||||
const getMacroDropdownValues = type => {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
return withNoneOption(teams.value);
|
||||
case 'send_email_to_team':
|
||||
return teams.value;
|
||||
case 'assign_agent':
|
||||
return [{ id: 'self', name: 'Self' }, ...agents.value];
|
||||
return [
|
||||
...withNoneOption(),
|
||||
{ id: 'self', name: 'Self' },
|
||||
...agents.value,
|
||||
];
|
||||
case 'add_label':
|
||||
case 'remove_label':
|
||||
return labels.value.map(i => ({
|
||||
|
||||
@@ -8,7 +8,7 @@ export const DEFAULT_MESSAGE_CREATED_CONDITION = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CONVERSATION_OPENED_CONDITION = [
|
||||
export const DEFAULT_CONVERSATION_CONDITION = [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
DEFAULT_MESSAGE_CREATED_CONDITION,
|
||||
DEFAULT_CONVERSATION_OPENED_CONDITION,
|
||||
DEFAULT_CONVERSATION_CONDITION,
|
||||
DEFAULT_OTHER_CONDITION,
|
||||
DEFAULT_ACTIONS,
|
||||
} from 'dashboard/constants/automation';
|
||||
@@ -169,8 +169,11 @@ export const getDefaultConditions = eventName => {
|
||||
if (eventName === 'message_created') {
|
||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
||||
}
|
||||
if (eventName === 'conversation_opened') {
|
||||
return DEFAULT_CONVERSATION_OPENED_CONDITION;
|
||||
if (
|
||||
eventName === 'conversation_opened' ||
|
||||
eventName === 'conversation_resolved'
|
||||
) {
|
||||
return DEFAULT_CONVERSATION_CONDITION;
|
||||
}
|
||||
return DEFAULT_OTHER_CONDITION;
|
||||
};
|
||||
|
||||
@@ -139,14 +139,14 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getInboxIconByType = (type, phoneNumber, variant = 'fill') => {
|
||||
export const getInboxIconByType = (type, medium, variant = 'fill') => {
|
||||
const iconMap =
|
||||
variant === 'fill' ? INBOX_ICON_MAP_FILL : INBOX_ICON_MAP_LINE;
|
||||
const defaultIcon =
|
||||
variant === 'fill' ? DEFAULT_ICON_FILL : DEFAULT_ICON_LINE;
|
||||
|
||||
// Special case for Twilio (whatsapp and sms)
|
||||
if (type === INBOX_TYPES.TWILIO && phoneNumber?.startsWith('whatsapp')) {
|
||||
if (type === INBOX_TYPES.TWILIO && medium === 'whatsapp') {
|
||||
return iconMap[INBOX_TYPES.WHATSAPP];
|
||||
}
|
||||
|
||||
|
||||
@@ -112,13 +112,13 @@ describe('#Inbox Helpers', () => {
|
||||
describe('Twilio cases', () => {
|
||||
describe('fill variant', () => {
|
||||
it('returns WhatsApp icon for Twilio WhatsApp number', () => {
|
||||
expect(
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp:+1234567890')
|
||||
).toBe('i-ri-whatsapp-fill');
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp')).toBe(
|
||||
'i-ri-whatsapp-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SMS icon for regular Twilio number', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890')).toBe(
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms')).toBe(
|
||||
'i-ri-chat-1-fill'
|
||||
);
|
||||
});
|
||||
@@ -133,18 +133,14 @@ describe('#Inbox Helpers', () => {
|
||||
describe('line variant', () => {
|
||||
it('returns WhatsApp line icon for Twilio WhatsApp number', () => {
|
||||
expect(
|
||||
getInboxIconByType(
|
||||
INBOX_TYPES.TWILIO,
|
||||
'whatsapp:+1234567890',
|
||||
'line'
|
||||
)
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp', 'line')
|
||||
).toBe('i-ri-whatsapp-line');
|
||||
});
|
||||
|
||||
it('returns SMS line icon for regular Twilio number', () => {
|
||||
expect(
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890', 'line')
|
||||
).toBe('i-ri-chat-1-line');
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms', 'line')).toBe(
|
||||
'i-ri-chat-1-line'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,10 @@ describe('#resolveTeamIds', () => {
|
||||
const resolvedTeams = '⚙️ sales team, 🤷♂️ fayaz';
|
||||
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
|
||||
});
|
||||
|
||||
it('resolves nil as None', () => {
|
||||
expect(resolveTeamIds(teams, ['nil'])).toEqual('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveLabels', () => {
|
||||
@@ -59,6 +63,10 @@ describe('#resolveAgents', () => {
|
||||
const resolvedAgents = 'John Doe';
|
||||
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
|
||||
});
|
||||
|
||||
it('resolves nil and self values', () => {
|
||||
expect(resolveAgents(agents, ['nil', 'self'])).toEqual('None, Self');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFileName', () => {
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
// Constants
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
export const DEFAULT_CATEGORY = 'UTILITY';
|
||||
export const COMPONENT_TYPES = {
|
||||
HEADER: 'HEADER',
|
||||
BODY: 'BODY',
|
||||
BUTTONS: 'BUTTONS',
|
||||
};
|
||||
export const MEDIA_FORMATS = ['IMAGE', 'VIDEO', 'DOCUMENT'];
|
||||
|
||||
export const findComponentByType = (template, type) =>
|
||||
template.components?.find(component => component.type === type);
|
||||
|
||||
export const processVariable = str => {
|
||||
return str.replace(/{{|}}/g, '');
|
||||
};
|
||||
@@ -17,12 +30,8 @@ export const replaceTemplateVariables = (templateText, processedParams) => {
|
||||
export const buildTemplateParameters = (template, hasMediaHeaderValue) => {
|
||||
const allVariables = {};
|
||||
|
||||
const bodyComponent = template.components.find(
|
||||
component => component.type === 'BODY'
|
||||
);
|
||||
const headerComponent = template.components.find(
|
||||
component => component.type === 'HEADER'
|
||||
);
|
||||
const bodyComponent = findComponentByType(template, COMPONENT_TYPES.BODY);
|
||||
const headerComponent = findComponentByType(template, COMPONENT_TYPES.HEADER);
|
||||
|
||||
if (!bodyComponent) return allVariables;
|
||||
|
||||
@@ -46,7 +55,7 @@ export const buildTemplateParameters = (template, hasMediaHeaderValue) => {
|
||||
|
||||
// Process button variables
|
||||
const buttonComponents = template.components.filter(
|
||||
component => component.type === 'BUTTONS'
|
||||
component => component.type === COMPONENT_TYPES.BUTTONS
|
||||
);
|
||||
|
||||
buttonComponents.forEach(buttonComponent => {
|
||||
|
||||
@@ -125,6 +125,7 @@ const validateSingleAction = action => {
|
||||
'mute_conversation',
|
||||
'snooze_conversation',
|
||||
'resolve_conversation',
|
||||
'remove_assigned_agent',
|
||||
'remove_assigned_team',
|
||||
'open_conversation',
|
||||
];
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Loading agents...",
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "Error while deleting portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Portal home page link",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Custom domain",
|
||||
"LABEL": "Custom domain:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Edit",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "Live",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Custom domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Send"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Connect",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Delete"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Cancel",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "جاري جلب الوكلاء...",
|
||||
"ASSIGN_TEAM": "تعيين فريق",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "معرف المحادثة {conversationId} تم تعيينه لـ \"{agentName}\"",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"FAILURE": "Could not delete account, try again!",
|
||||
"SCHEDULED_DELETION": {
|
||||
"TITLE": "Account Scheduled for Deletion",
|
||||
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
|
||||
"MESSAGE_MANUAL": "هذا الحساب مجدول للحذف بتاريخ {deletionDate}. تم طلبه من قبل المسؤول. يمكنك إلغاء الحذف قبل هذا التاريخ.",
|
||||
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
|
||||
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
|
||||
}
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "حدث خطأ أثناء حذف البوابة"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "نطاق مخصص",
|
||||
"LABEL": "نطاق مخصص:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "نطاق البوابة المخصص",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "تعديل",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "مباشر",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "نطاق مخصص",
|
||||
"PLACEHOLDER": "نطاق البوابة المخصص",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "إرسال"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "تم تحديد جميع الإشعارات كمقروءة",
|
||||
"DELETE_ALL": "تم حذف كل الإشعارات",
|
||||
"DELETE_ALL_READ": "تم حذف كل الإشعارات المقروءة"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "ربط الاتصال",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "رمز التحقق من Webhook",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "حذف"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "العنوان",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "الوصف",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "إنشاء",
|
||||
"CANCEL": "إلغاء"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "إلغاء",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Loading agents...",
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "Error while deleting portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Portal home page link",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Custom domain",
|
||||
"LABEL": "Custom domain:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Edit",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "Live",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Custom domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Send"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Connect",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Delete"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Cancel",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Loading agents...",
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "Error while deleting portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Portal home page link",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Custom domain",
|
||||
"LABEL": "Custom domain:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Редактирай",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "Live",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Custom domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Send"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Connect",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Изтрий"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Описание",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Отмени"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Отмени",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "S'estan carregant els agents...",
|
||||
"ASSIGN_TEAM": "Assigna un equip",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Id de conversa {conversationId} assignat a \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal esborrat correctament",
|
||||
"DELETE_ERROR": "S'ha produït un error en suprimir el portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Enllaç a la pàgina d'inici del portal",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Domini personalitzat",
|
||||
"LABEL": "Domini personalitzat:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Domini personalitzat del portal",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Edita",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "En directe",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Domini personalitzat",
|
||||
"PLACEHOLDER": "Domini personalitzat del portal",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Envia"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "Totes les notificacions s'han marcat com a llegides",
|
||||
"DELETE_ALL": "S'han suprimit totes les notificacions",
|
||||
"DELETE_ALL_READ": "S'han suprimit totes les notificacions de lectura"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Actualitza l'API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Introdueix la nova clau de l'API aquí",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualitza",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Connectar",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Token de verificació del webhook",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Aquest testimoni s'utilitza per verificar l'autenticitat del punt final del webhook.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Esborrar"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Títol",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Descripció",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Crear",
|
||||
"CANCEL": "Cancel·la"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Cancel·la",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Načítání agentů...",
|
||||
"ASSIGN_TEAM": "Přiřadit tým",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Konverzace id {conversationId} přiřazena \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "Error while deleting portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Portal home page link",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Custom domain",
|
||||
"LABEL": "Custom domain:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Upravit",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "Live",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Custom domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Poslat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualizovat",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Connect",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Vymazat"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Zrušit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Zrušit",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Indlæser agenter...",
|
||||
"ASSIGN_TEAM": "Tildel team",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Samtale id {conversationId} tildelt \"{agentName}\"",
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
"DELETE_SUCCESS": "Portal slettet",
|
||||
"DELETE_ERROR": "Fejl under sletning af portal"
|
||||
}
|
||||
},
|
||||
"SEND_CNAME_INSTRUCTIONS": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -726,7 +732,7 @@
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home page link",
|
||||
"PLACEHOLDER": "Link til portalens hjemmeside",
|
||||
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
|
||||
},
|
||||
"SLUG": {
|
||||
"LABEL": "Snegl",
|
||||
@@ -747,9 +753,15 @@
|
||||
"HEADER": "Tilpasset domæne",
|
||||
"LABEL": "Tilpasset domæne:",
|
||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||
"PLACEHOLDER": "Portal brugerdefineret domæne",
|
||||
"EDIT_BUTTON": "Edit custom domain",
|
||||
"EDIT_BUTTON": "Rediger",
|
||||
"ADD_BUTTON": "Add custom domain",
|
||||
"STATUS": {
|
||||
"LIVE": "Levende",
|
||||
"PENDING": "Awaiting verification",
|
||||
"ERROR": "Verification failed"
|
||||
},
|
||||
"DIALOG": {
|
||||
"ADD_HEADER": "Add custom domain",
|
||||
"EDIT_HEADER": "Edit custom domain",
|
||||
@@ -757,13 +769,20 @@
|
||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||
"LABEL": "Tilpasset domæne",
|
||||
"PLACEHOLDER": "Portal brugerdefineret domæne",
|
||||
"ERROR": "Custom domain is required"
|
||||
"ERROR": "Custom domain is required",
|
||||
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||
},
|
||||
"DNS_CONFIGURATION_DIALOG": {
|
||||
"HEADER": "DNS configuration",
|
||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
||||
"COPY": "Successfully copied CNAME",
|
||||
"SEND_INSTRUCTIONS": {
|
||||
"HEADER": "Send instructions",
|
||||
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||
"PLACEHOLDER": "Enter their email",
|
||||
"ERROR": "Enter a valid email address",
|
||||
"SEND_BUTTON": "Send"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
|
||||
@@ -72,6 +72,24 @@
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
},
|
||||
"REAUTHORIZE": {
|
||||
"TITLE": "Reauthorization Required",
|
||||
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
|
||||
"BUTTON_TEXT": "Reconnect WhatsApp",
|
||||
"LOADING_FACEBOOK": "Loading Facebook SDK...",
|
||||
"SUCCESS": "WhatsApp reconnected successfully",
|
||||
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
|
||||
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
|
||||
"TROUBLESHOOTING": {
|
||||
"TITLE": "Troubleshooting",
|
||||
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
|
||||
"COOKIES": "Third-party cookies must be enabled",
|
||||
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||
},
|
||||
"LEARN_MORE": {
|
||||
"TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
|
||||
"LINK_TEXT": "this link.",
|
||||
"LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
@@ -598,6 +603,21 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Opdater",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
|
||||
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
|
||||
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
|
||||
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
|
||||
"WHATSAPP_CONNECT_BUTTON": "Tilslut",
|
||||
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
|
||||
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
|
||||
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
|
||||
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
|
||||
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook verifikations token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||
|
||||
@@ -557,6 +557,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Guardrails added successfully",
|
||||
@@ -604,6 +605,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Response Guidelines added successfully",
|
||||
@@ -618,6 +620,73 @@
|
||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SCENARIOS": {
|
||||
"TITLE": "Scenarios",
|
||||
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||
"BREADCRUMB": {
|
||||
"TITLE": "Scenarios"
|
||||
},
|
||||
"BULK_ACTION": {
|
||||
"SELECTED": "{count} item selected | {count} items selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "Slet"
|
||||
},
|
||||
"ADD": {
|
||||
"SUGGESTED": {
|
||||
"TITLE": "Example scenarios",
|
||||
"ADD": "Add all",
|
||||
"ADD_SINGLE": "Add this",
|
||||
"TOOLS_USED": "Tools used :"
|
||||
},
|
||||
"NEW": {
|
||||
"CREATE": "Add a scenario",
|
||||
"TITLE": "Create a scenario",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Titel",
|
||||
"PLACEHOLDER": "Enter a name for the scenario",
|
||||
"ERROR": "Scenario name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Beskrivelse",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||
"ERROR": "Scenario description is required"
|
||||
},
|
||||
"INSTRUCTION": {
|
||||
"LABEL": "How to handle",
|
||||
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||
"ERROR": "Scenario content is required"
|
||||
},
|
||||
"CREATE": "Opret",
|
||||
"CANCEL": "Annuller"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UPDATE": {
|
||||
"CANCEL": "Annuller",
|
||||
"UPDATE": "Update changes"
|
||||
},
|
||||
"LIST": {
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||
"API": {
|
||||
"ADD": {
|
||||
"SUCCESS": "Scenarios added successfully",
|
||||
"ERROR": "There was an error adding scenarios, please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Scenarios updated successfully",
|
||||
"ERROR": "There was an error updating scenarios, please try again."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Scenarios deleted successfully",
|
||||
"ERROR": "There was an error deleting scenarios, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DOCUMENTS": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
"AGENTS_LOADING": "Agenten werden geladen...",
|
||||
"ASSIGN_TEAM": "Team zuweisen",
|
||||
"DELETE": "Unterhaltung löschen",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Konversations-ID {conversationId} zugewiesen zu \"{agentName}\"",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user