Compare commits
227
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30e4585af2 | ||
|
|
3dba5f55a3 | ||
|
|
42d14ae4d0 | ||
|
|
2ed7b53ca3 | ||
|
|
e82ba57464 | ||
|
|
adc260ab27 | ||
|
|
ad65bace3b | ||
|
|
1b39d151f4 | ||
|
|
8e1fa6e03d | ||
|
|
ba0b526009 | ||
|
|
0fa5a75302 | ||
|
|
7c8bd0ca91 | ||
|
|
c05cd06d96 | ||
|
|
07438acad5 | ||
|
|
5e591b9ceb | ||
|
|
260d16afe1 | ||
|
|
d22c1fc4b7 | ||
|
|
1f69cfbb5c | ||
|
|
8badb131b8 | ||
|
|
0e4c352571 | ||
|
|
bb4a7cf2f2 | ||
|
|
04e667b0a4 | ||
|
|
b1c97d4846 | ||
|
|
88ac480da9 | ||
|
|
7694ca0cc5 | ||
|
|
7ea9c402a1 | ||
|
|
8df10a9397 | ||
|
|
65ba5c2eae | ||
|
|
d999e955d6 | ||
|
|
6143d9dea7 | ||
|
|
854c9bcf55 | ||
|
|
19702eda8b | ||
|
|
75a51786ec | ||
|
|
3d7cfd4c85 | ||
|
|
a3bf14955c | ||
|
|
1980fdf278 | ||
|
|
0ac2be508d | ||
|
|
2fe2d04208 | ||
|
|
ccc0357fee | ||
|
|
b934297a99 | ||
|
|
63bec71927 | ||
|
|
902d268ba7 | ||
|
|
c610e288cb | ||
|
|
5cb7978997 | ||
|
|
c87cfbafbc | ||
|
|
f6e6c027cd | ||
|
|
21bc4167fa | ||
|
|
abffd343f1 | ||
|
|
b362ee3e3c | ||
|
|
b95e1b94a7 | ||
|
|
be7469f342 | ||
|
|
602af113a1 | ||
|
|
313222c52a | ||
|
|
1e80e88bb7 | ||
|
|
d3c4e95edf | ||
|
|
429fba6ddd | ||
|
|
a9d80705cb | ||
|
|
01ad16dab8 | ||
|
|
7877d6a080 | ||
|
|
734b1f5a3d | ||
|
|
38cc568bb0 | ||
|
|
ed8c24c043 | ||
|
|
9855995480 | ||
|
|
1c6e64b2f0 | ||
|
|
f8ad752c86 | ||
|
|
ef31d99431 | ||
|
|
44fc22ab5c | ||
|
|
3f05d8972e | ||
|
|
25a731eb6c | ||
|
|
063460b198 | ||
|
|
670545b317 | ||
|
|
3da8ae601d | ||
|
|
919154e26c | ||
|
|
8859cdff07 | ||
|
|
e8e4d04073 | ||
|
|
f2dd1e7806 | ||
|
|
597158c221 | ||
|
|
b276344f0c | ||
|
|
e9a882e7bc | ||
|
|
f6f757c358 | ||
|
|
92723b0d23 | ||
|
|
e1b1e1f286 | ||
|
|
6b2a4d70e8 | ||
|
|
a9767234f9 | ||
|
|
88cb9c22f0 | ||
|
|
913547c3b4 | ||
|
|
714ba2d823 | ||
|
|
a08c286871 | ||
|
|
436a63f584 | ||
|
|
e87ccfac50 | ||
|
|
c5044fce96 | ||
|
|
07e0b35529 | ||
|
|
a614a8e327 | ||
|
|
d96b79855c | ||
|
|
194bb7b317 | ||
|
|
3ed140ef80 | ||
|
|
06f2db89af | ||
|
|
582a984ea3 | ||
|
|
ced1532a31 | ||
|
|
8b7a0528aa | ||
|
|
c86a9e1236 | ||
|
|
8292d611cd | ||
|
|
7d09ab7086 | ||
|
|
956ed80cbd | ||
|
|
5a3d38b854 | ||
|
|
088f48f401 | ||
|
|
37bbde5600 | ||
|
|
d31e72254f | ||
|
|
558b1c9787 | ||
|
|
293bdbdb2e | ||
|
|
aca0e1ae6d | ||
|
|
bdc6d15e62 | ||
|
|
a640c127b2 | ||
|
|
3ee3dd0df6 | ||
|
|
75260980c5 | ||
|
|
7d46f51669 | ||
|
|
f28e1bd016 | ||
|
|
619edb5eb0 | ||
|
|
153b5675ca | ||
|
|
60c623121d | ||
|
|
eb49cbec51 | ||
|
|
e80510632c | ||
|
|
5f66f4b0a7 | ||
|
|
0fa18cddda | ||
|
|
fd6ad2b1e1 | ||
|
|
04d5279411 | ||
|
|
f7058a8af4 | ||
|
|
eecfd8132b | ||
|
|
70ed790098 | ||
|
|
6c3e667fac | ||
|
|
7494baef3c | ||
|
|
f2034c752f | ||
|
|
2d489c1c0a | ||
|
|
53ede69a73 | ||
|
|
c2e2b320ef | ||
|
|
cb05a318a7 | ||
|
|
fd9beedb68 | ||
|
|
e2061fb624 | ||
|
|
ce5803d312 | ||
|
|
cba2647b5e | ||
|
|
c3e438b365 | ||
|
|
a48fea0a7f | ||
|
|
80daea6054 | ||
|
|
cd93459320 | ||
|
|
9801137296 | ||
|
|
d4890a6e52 | ||
|
|
79c8df7c4f | ||
|
|
e204ed95ce | ||
|
|
34a3da97b2 | ||
|
|
5da34f916e | ||
|
|
4a31dd2120 | ||
|
|
6ca32faeab | ||
|
|
186d8c9462 | ||
|
|
50a46d123a | ||
|
|
ec5dee5c46 | ||
|
|
93cc1f339a | ||
|
|
3468ca48c3 | ||
|
|
47e2e4371f | ||
|
|
9f3d2d545b | ||
|
|
4e04fd3d08 | ||
|
|
e064899267 | ||
|
|
3594ce9bb5 | ||
|
|
03777d7cc1 | ||
|
|
e3e238bce8 | ||
|
|
e0e8a70720 | ||
|
|
e2d1ce7554 | ||
|
|
1dd5eb64cf | ||
|
|
0c6d2cc0b7 | ||
|
|
b6ff4f54be | ||
|
|
07c92c6097 | ||
|
|
cafa486c18 | ||
|
|
baea194f6b | ||
|
|
583ba7c2a0 | ||
|
|
1a54cc4990 | ||
|
|
60099a4d5b | ||
|
|
dcb2ab7c99 | ||
|
|
aa5735df39 | ||
|
|
81bd22d0dd | ||
|
|
f8ff3b8d74 | ||
|
|
20ccca72ae | ||
|
|
60fafc65ca | ||
|
|
f53ae6457c | ||
|
|
2bb9886c6d | ||
|
|
9b9875b847 | ||
|
|
7e5f19e2b0 | ||
|
|
0d73760dac | ||
|
|
8527170d82 | ||
|
|
044282730f | ||
|
|
d5bf7f81cb | ||
|
|
8b125efac2 | ||
|
|
69ea2dc58d | ||
|
|
5cc2004a71 | ||
|
|
92ac1c3825 | ||
|
|
bc8f528f90 | ||
|
|
bef1a6047a | ||
|
|
ab4eb0e7d7 | ||
|
|
e5b64287ac | ||
|
|
34a480e799 | ||
|
|
58b067c08f | ||
|
|
78faf125bd | ||
|
|
0d9662477d | ||
|
|
3e31f63f09 | ||
|
|
b84d98b90b | ||
|
|
3a75ec02f0 | ||
|
|
9cbe749994 | ||
|
|
e9ea5cbe45 | ||
|
|
665549d52d | ||
|
|
17db86c2fe | ||
|
|
0ace7f38a6 | ||
|
|
4a2250065c | ||
|
|
31ba25b12b | ||
|
|
bc5bd51003 | ||
|
|
6d933112e7 | ||
|
|
45d7e570b7 | ||
|
|
bb3ad70f0e | ||
|
|
c983c87537 | ||
|
|
c5def0522f | ||
|
|
b95e59ee0d | ||
|
|
f49d825e8b | ||
|
|
75a82d9ab7 | ||
|
|
0eb121df75 | ||
|
|
18e45a0da6 | ||
|
|
df14e97ff6 | ||
|
|
63c9cf3ad1 | ||
|
|
ec36442f57 | ||
|
|
b81c43a5a2 | ||
|
|
23cad33d96 |
@@ -76,7 +76,7 @@ gem 'faraday_middleware-aws-sigv4'
|
||||
##--- gems for server & infra configuration ---##
|
||||
gem 'dotenv-rails', '>= 3.0.0'
|
||||
gem 'foreman'
|
||||
gem 'puma', '~> 7.2', '>= 7.2.1'
|
||||
gem 'puma'
|
||||
gem 'vite_rails'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
+23
-34
@@ -136,8 +136,6 @@ GEM
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
activesupport (>= 5.0, < 7.7)
|
||||
auth-sanitizer (0.2.1)
|
||||
version_gem (~> 1.1, >= 1.1.10)
|
||||
aws-actionmailbox-ses (0.1.0)
|
||||
actionmailbox (>= 7.1.0)
|
||||
aws-sdk-s3 (~> 1, >= 1.123.0)
|
||||
@@ -170,7 +168,7 @@ GEM
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.22)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -186,7 +184,6 @@ GEM
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
cld3 (3.7.0)
|
||||
@@ -315,7 +312,7 @@ GEM
|
||||
hashie
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.4)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
faraday (~> 2.5)
|
||||
@@ -438,8 +435,7 @@ GEM
|
||||
hana (1.3.7)
|
||||
hash_diff (1.1.1)
|
||||
hashdiff (1.1.0)
|
||||
hashie (5.1.0)
|
||||
logger
|
||||
hashie (5.0.0)
|
||||
html2text (0.4.0)
|
||||
nokogiri (>= 1.0, < 2.0)
|
||||
http (5.1.1)
|
||||
@@ -474,7 +470,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.19.8)
|
||||
json (2.19.5)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
@@ -572,7 +568,7 @@ GEM
|
||||
ruby2_keywords
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.9.1)
|
||||
multi_xml (0.8.0)
|
||||
bigdecimal (>= 3.1, < 5)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
@@ -597,7 +593,7 @@ GEM
|
||||
sidekiq
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.5)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.19.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
@@ -607,26 +603,19 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.6)
|
||||
auth-sanitizer (~> 0.2, >= 0.2.1)
|
||||
base64 (~> 0.1)
|
||||
cgi
|
||||
oauth-tty (~> 1.0, >= 1.0.8)
|
||||
snaky_hash (~> 2.0, >= 2.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.11)
|
||||
oauth-tty (1.0.8)
|
||||
auth-sanitizer (~> 0.1, >= 0.1.3)
|
||||
cgi
|
||||
version_gem (~> 1.1, >= 1.1.9)
|
||||
oauth2 (2.0.22)
|
||||
auth-sanitizer (~> 0.2, >= 0.2.1)
|
||||
faraday (>= 0.17.3, < 4.0)
|
||||
jwt (>= 1.0, < 4.0)
|
||||
logger (~> 1.2)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oauth-tty (1.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
oauth2 (2.0.9)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
jwt (>= 1.0, < 3.0)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0, >= 2.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.11)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oj (3.16.10)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
@@ -693,7 +682,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (7.0.5)
|
||||
puma (7.2.1)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -946,9 +935,9 @@ GEM
|
||||
gli
|
||||
hashie
|
||||
logger
|
||||
snaky_hash (2.0.5)
|
||||
hashie (>= 0.1.0, < 6)
|
||||
version_gem (>= 1.1.8, < 3)
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sorbet-runtime (0.5.11934)
|
||||
spring (4.1.1)
|
||||
spring-watcher-listen (2.1.0)
|
||||
@@ -1006,7 +995,7 @@ GEM
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.11)
|
||||
version_gem (1.1.4)
|
||||
vite_rails (3.10.0)
|
||||
railties (>= 5.1, < 9)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
@@ -1143,7 +1132,7 @@ DEPENDENCIES
|
||||
pgvector
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma (~> 7.2, >= 7.2.1)
|
||||
puma
|
||||
pundit
|
||||
rack-attack (>= 6.7.0)
|
||||
rack-cors (= 2.0.0)
|
||||
|
||||
@@ -6,7 +6,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
||||
{
|
||||
redirect_uri: "#{base_url}/microsoft/callback",
|
||||
scope: scope,
|
||||
state: state
|
||||
state: state,
|
||||
prompt: 'consent'
|
||||
}
|
||||
)
|
||||
if redirect_url
|
||||
|
||||
@@ -80,15 +80,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
:id, :color, :custom_domain, :header_text, :homepage_link,
|
||||
:name, :page_title, :slug, :archived,
|
||||
{ config: [:default_locale, :layout, { allowed_locales: [] }, { draft_locales: [] },
|
||||
{ social_profiles: %i[facebook x instagram linkedin youtube tiktok github whatsapp] },
|
||||
{ locale_translations: locale_translation_keys.index_with { %i[name page_title header_text] } }] }
|
||||
{ social_profiles: %i[facebook x instagram linkedin youtube tiktok github whatsapp] }] }
|
||||
)
|
||||
end
|
||||
|
||||
def locale_translation_keys
|
||||
params.dig(:portal, :config, :locale_translations)&.keys || []
|
||||
end
|
||||
|
||||
def live_chat_widget_params
|
||||
permitted_params = params.permit(:inbox_id)
|
||||
return {} unless permitted_params.key?(:inbox_id)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => %w[show toggle_status toggle_typing_status toggle_priority create update custom_attributes],
|
||||
'api/v1/accounts/conversations' => %w[toggle_status toggle_typing_status toggle_priority create update custom_attributes],
|
||||
'api/v1/accounts/conversations/messages' => ['create'],
|
||||
'api/v1/accounts/conversations/assignments' => ['create'],
|
||||
'api/v1/accounts/conversations/labels' => %w[index create]
|
||||
'api/v1/accounts/conversations/assignments' => ['create']
|
||||
}.freeze
|
||||
|
||||
def ensure_access_token
|
||||
|
||||
@@ -9,7 +9,7 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
|
||||
layout 'portal'
|
||||
|
||||
def show
|
||||
@og_image_url = helpers.set_og_image_url('', @portal.localized_value('header_text', @locale))
|
||||
@og_image_url = helpers.set_og_image_url('', @portal.header_text)
|
||||
end
|
||||
|
||||
def sitemap
|
||||
|
||||
@@ -12,7 +12,7 @@ class ContactDrop < BaseDrop
|
||||
end
|
||||
|
||||
def first_name
|
||||
@obj.try(:name).try(:split).try(:first).try(:capitalize)
|
||||
@obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||
end
|
||||
|
||||
def last_name
|
||||
|
||||
@@ -12,7 +12,7 @@ class UserDrop < BaseDrop
|
||||
end
|
||||
|
||||
def first_name
|
||||
@obj.try(:name).try(:split).try(:first).try(:capitalize)
|
||||
@obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size).to_i > 1
|
||||
end
|
||||
|
||||
def last_name
|
||||
|
||||
@@ -34,12 +34,6 @@ disable_branding:
|
||||
enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
|
||||
icon: 'icon-sailbot-fill'
|
||||
enterprise: true
|
||||
voice_calls:
|
||||
name: 'Voice Calls'
|
||||
description: 'Enable voice calling capabilities for your agents and customers.'
|
||||
enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
|
||||
icon: 'icon-voice-line'
|
||||
enterprise: true
|
||||
|
||||
# ------- Product Features ------- #
|
||||
help_center:
|
||||
|
||||
@@ -48,12 +48,6 @@ class TwilioVoiceClient extends EventTarget {
|
||||
return !!this.activeConnection;
|
||||
}
|
||||
|
||||
setMuted(shouldMute) {
|
||||
if (!this.activeConnection) return false;
|
||||
this.activeConnection.mute(shouldMute);
|
||||
return shouldMute;
|
||||
}
|
||||
|
||||
endClientCall() {
|
||||
if (this.activeConnection) {
|
||||
this.activeConnection.disconnect();
|
||||
|
||||
@@ -46,8 +46,9 @@ const formattedLastActivityAt = computed(() => {
|
||||
:src="avatarSource"
|
||||
class="shrink-0"
|
||||
:name="name"
|
||||
:size="42"
|
||||
:size="48"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
|
||||
|
||||
+1
@@ -141,6 +141,7 @@ const handleUpdateCompany = async () => {
|
||||
:src="avatarSource"
|
||||
:size="72"
|
||||
:allow-upload="!isAvatarBusy"
|
||||
rounded-full
|
||||
hide-offline-status
|
||||
@upload="handleAvatarUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
|
||||
@@ -124,9 +124,10 @@ const handleAvatarHover = isHovered => {
|
||||
<Avatar
|
||||
:name="name"
|
||||
:src="thumbnail"
|
||||
:size="42"
|
||||
:size="48"
|
||||
:status="availabilityStatus"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
>
|
||||
<template v-if="selectable" #overlay="{ size }">
|
||||
<label
|
||||
|
||||
@@ -53,9 +53,6 @@ const localeMenuLabels = computed(() => ({
|
||||
'publish-locale': t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.PUBLISH_LOCALE'
|
||||
),
|
||||
'customize-content': t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.CUSTOMIZE_CONTENT'
|
||||
),
|
||||
delete: t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE'),
|
||||
}));
|
||||
|
||||
@@ -131,7 +128,7 @@ const handleAction = ({ action, value }) => {
|
||||
<DropdownMenu
|
||||
v-if="showDropdownMenu"
|
||||
:menu-items="localeMenuItems"
|
||||
class="ltr:right-0 rtl:left-0 mt-1 top-full z-60 min-w-[150px]"
|
||||
class="ltr:right-0 rtl:left-0 mt-1 xl:ltr:left-0 xl:rtl:right-0 top-full z-60 min-w-[150px]"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const activeLocale = ref('');
|
||||
const name = ref('');
|
||||
const pageTitle = ref('');
|
||||
const headerText = ref('');
|
||||
|
||||
const localeTranslations = computed(
|
||||
() => props.portal?.config?.locale_translations || {}
|
||||
);
|
||||
|
||||
const openForLocale = localeCode => {
|
||||
const existing = localeTranslations.value[localeCode] || {};
|
||||
activeLocale.value = localeCode;
|
||||
name.value = existing.name || '';
|
||||
pageTitle.value = existing.page_title || '';
|
||||
headerText.value = existing.header_text || '';
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const onConfirm = async () => {
|
||||
const translations = { ...localeTranslations.value };
|
||||
const fields = {};
|
||||
if (name.value.trim()) fields.name = name.value.trim();
|
||||
if (pageTitle.value.trim()) fields.page_title = pageTitle.value.trim();
|
||||
if (headerText.value.trim()) fields.header_text = headerText.value.trim();
|
||||
|
||||
if (Object.keys(fields).length) {
|
||||
translations[activeLocale.value] = fields;
|
||||
} else {
|
||||
delete translations[activeLocale.value];
|
||||
}
|
||||
|
||||
try {
|
||||
await store.dispatch('portals/update', {
|
||||
portalSlug: props.portal?.slug,
|
||||
config: { locale_translations: translations },
|
||||
});
|
||||
dialogRef.value?.close();
|
||||
useAlert(t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ openForLocale });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.TITLE')"
|
||||
:description="t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.DESCRIPTION')"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.NAME.LABEL') }}
|
||||
</label>
|
||||
<Input v-model="name" :placeholder="portal.name" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.PAGE_TITLE.LABEL') }}
|
||||
</label>
|
||||
<Input v-model="pageTitle" :placeholder="portal.page_title" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('HELP_CENTER.LOCALES_PAGE.CONTENT_DIALOG.HEADER_TEXT.LABEL') }}
|
||||
</label>
|
||||
<Input v-model="headerText" :placeholder="portal.header_text" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
|
||||
import LocaleContentDialog from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleContentDialog.vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -25,8 +23,6 @@ const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const contentDialogRef = ref(null);
|
||||
|
||||
const isLocaleDefault = code => {
|
||||
return props.portal?.meta?.default_locale === code;
|
||||
};
|
||||
@@ -152,8 +148,6 @@ const handleAction = ({ action }, localeCode) => {
|
||||
moveLocaleToDraft({ localeCode: localeCode });
|
||||
} else if (action === 'publish-locale') {
|
||||
publishLocale({ localeCode: localeCode });
|
||||
} else if (action === 'customize-content') {
|
||||
contentDialogRef.value.openForLocale(localeCode);
|
||||
} else if (action === 'delete') {
|
||||
deletePortalLocale({ localeCode: localeCode });
|
||||
}
|
||||
@@ -173,6 +167,5 @@ const handleAction = ({ action }, localeCode) => {
|
||||
:category-count="locale.categoriesCount || 0"
|
||||
@action="handleAction($event, locale.code)"
|
||||
/>
|
||||
<LocaleContentDialog ref="contentDialogRef" :portal="portal" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
+1
-6
@@ -182,10 +182,7 @@ const MIN_SEARCH_LENGTH = 2;
|
||||
export const createContactSearcher = () => {
|
||||
let controller = null;
|
||||
|
||||
return async (
|
||||
query,
|
||||
{ skipMinLength = false, reachableOnly = true } = {}
|
||||
) => {
|
||||
return async (query, { skipMinLength = false } = {}) => {
|
||||
const trimmed = typeof query === 'string' ? query.trim() : '';
|
||||
|
||||
controller?.abort();
|
||||
@@ -202,8 +199,6 @@ export const createContactSearcher = () => {
|
||||
} = await ContactAPI.search(trimmed, 1, 'name', '', { signal });
|
||||
|
||||
const camelCasedPayload = camelcaseKeys(payload, { deep: true });
|
||||
if (!reachableOnly) return camelCasedPayload || [];
|
||||
|
||||
// Filter contacts that have either phone_number or email
|
||||
const filteredPayload = camelCasedPayload?.filter(
|
||||
contact => contact.phoneNumber || contact.email
|
||||
|
||||
@@ -197,9 +197,9 @@ const channelIcon = computed(() => {
|
||||
? $t('CONVERSATION.VOICE_WIDGET.END_CALL')
|
||||
: $t('CONVERSATION.VOICE_WIDGET.REJECT_CALL')
|
||||
"
|
||||
icon="i-ph-phone-bold"
|
||||
icon="i-ph-phone-x-bold"
|
||||
ruby
|
||||
class="!rounded-full rotate-[135deg]"
|
||||
class="!rounded-full rotate-[134deg]"
|
||||
@click="isOngoing ? $emit('end') : $emit('reject')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { useCallSession } from 'dashboard/composables/useCallSession';
|
||||
import { setWhatsappCallMuted } from 'dashboard/composables/useWhatsappCallSession';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
||||
import { VOICE_CALL_PROVIDERS } from 'dashboard/helper/inbox';
|
||||
import { VOICE_CALL_DIRECTION } from 'dashboard/components-next/message/constants';
|
||||
@@ -12,7 +11,7 @@ import WindowVisibilityHelper from 'dashboard/helper/AudioAlerts/WindowVisibilit
|
||||
import CallCard from 'dashboard/components-next/call/CallCard.vue';
|
||||
import countriesList from 'shared/constants/countries.js';
|
||||
|
||||
const RINGTONE_URL = '/audio/dashboard/ringtone.mp3';
|
||||
const RINGTONE_URL = '/audio/dashboard/bell.mp3';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -30,8 +29,8 @@ const {
|
||||
formattedCallDuration,
|
||||
} = useCallSession();
|
||||
|
||||
// Mute routes by provider: WhatsApp toggles the local mic track, Twilio uses
|
||||
// the Voice SDK connection's native mute. Both surface the same button.
|
||||
// Mute is currently WhatsApp-only — Twilio calls are mediated server-side and
|
||||
// don't expose a mic track on the browser side.
|
||||
const isMuted = ref(false);
|
||||
const isWhatsappActive = computed(
|
||||
() => activeCall.value?.provider === VOICE_CALL_PROVIDERS.WHATSAPP
|
||||
@@ -64,11 +63,7 @@ const stackedCardState = call =>
|
||||
|
||||
const toggleMute = () => {
|
||||
isMuted.value = !isMuted.value;
|
||||
if (isWhatsappActive.value) {
|
||||
setWhatsappCallMuted(isMuted.value);
|
||||
} else {
|
||||
TwilioVoiceClient.setMuted(isMuted.value);
|
||||
}
|
||||
setWhatsappCallMuted(isMuted.value);
|
||||
};
|
||||
|
||||
watch(hasActiveCall, active => {
|
||||
@@ -261,7 +256,7 @@ onBeforeUnmount(stopRingtone);
|
||||
:call-info="getCallInfo(activeCall || primaryIncomingCall)"
|
||||
:duration="hasActiveCall ? formattedCallDuration : ''"
|
||||
:is-muted="isMuted"
|
||||
:show-mute="hasActiveCall"
|
||||
:show-mute="hasActiveCall && isWhatsappActive"
|
||||
@accept="handleJoinCall(primaryIncomingCall)"
|
||||
@reject="rejectIncomingCall(primaryIncomingCall?.callSid)"
|
||||
@dismiss="dismissCall(primaryIncomingCall?.callSid)"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, h, watch, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import Button from 'next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import FilterSelect from './inputs/FilterSelect.vue';
|
||||
@@ -110,34 +109,6 @@ const inputFieldType = computed(() => {
|
||||
return 'text';
|
||||
});
|
||||
|
||||
const asyncOptions = ref([]);
|
||||
const isSearching = ref(false);
|
||||
const lastSearchQuery = ref('');
|
||||
|
||||
const performAsyncSearch = async query => {
|
||||
let results;
|
||||
try {
|
||||
results = await currentFilter.value.searchOptions(query);
|
||||
} catch {
|
||||
results = [];
|
||||
}
|
||||
// skip stale responses — a newer search in this row owns the UI
|
||||
if (query !== lastSearchQuery.value) return;
|
||||
// null means another row's search aborted ours, reset instead of staying stuck on the searching state
|
||||
if (results !== null) asyncOptions.value = results;
|
||||
isSearching.value = false;
|
||||
};
|
||||
|
||||
const debouncedAsyncSearch = debounce(performAsyncSearch, 300);
|
||||
|
||||
const onAsyncSearch = query => {
|
||||
const hasQuery = !!query.trim();
|
||||
lastSearchQuery.value = query;
|
||||
if (!hasQuery) asyncOptions.value = [];
|
||||
isSearching.value = hasQuery;
|
||||
debouncedAsyncSearch(query);
|
||||
};
|
||||
|
||||
const resetModelOnAttributeKeyChange = newAttributeKey => {
|
||||
/**
|
||||
* Resets the filter values and operator when the attribute key changes. This ensures that
|
||||
@@ -150,16 +121,11 @@ const resetModelOnAttributeKeyChange = newAttributeKey => {
|
||||
const newInputType = getInputType(newOperator, filter);
|
||||
if (newInputType === 'multiSelect') {
|
||||
values.value = [];
|
||||
} else if (
|
||||
['searchSelect', 'asyncSearchSelect', 'booleanSelect'].includes(
|
||||
newInputType
|
||||
)
|
||||
) {
|
||||
} else if (['searchSelect', 'booleanSelect'].includes(newInputType)) {
|
||||
values.value = {};
|
||||
} else {
|
||||
values.value = '';
|
||||
}
|
||||
asyncOptions.value = [];
|
||||
filterOperator.value = newOperator.value;
|
||||
};
|
||||
|
||||
@@ -219,16 +185,6 @@ defineExpose({ validate, resetValidation });
|
||||
:options="currentFilter.options"
|
||||
dropdown-max-height="max-h-64"
|
||||
/>
|
||||
<SingleSelect
|
||||
v-else-if="inputType === 'asyncSearchSelect'"
|
||||
v-model="values"
|
||||
async-search
|
||||
:options="asyncOptions"
|
||||
:is-searching="isSearching"
|
||||
:search-placeholder="currentFilter.searchPlaceholder"
|
||||
dropdown-max-height="max-h-64"
|
||||
@search="onAsyncSearch"
|
||||
/>
|
||||
<SingleSelect
|
||||
v-else-if="inputType === 'booleanSelect'"
|
||||
v-model="values"
|
||||
|
||||
@@ -7,7 +7,6 @@ export const CONVERSATION_ATTRIBUTES = {
|
||||
ASSIGNEE_ID: 'assignee_id',
|
||||
INBOX_ID: 'inbox_id',
|
||||
TEAM_ID: 'team_id',
|
||||
CONTACT_ID: 'contact_id',
|
||||
DISPLAY_ID: 'display_id',
|
||||
CAMPAIGN_ID: 'campaign_id',
|
||||
LABELS: 'labels',
|
||||
|
||||
@@ -1,37 +1,9 @@
|
||||
import { ref } from 'vue';
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import { useConversationFilterContext } from '../provider';
|
||||
import {
|
||||
CONVERSATION_ATTRIBUTES,
|
||||
getCustomAttributeInputType,
|
||||
buildAttributesFilterTypes,
|
||||
replaceUnderscoreWithSpace,
|
||||
} from './filterHelper';
|
||||
|
||||
vi.mock('dashboard/api/contacts', () => ({
|
||||
default: {
|
||||
search: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/store.js', () => ({
|
||||
useMapGetter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('next/icon/provider', () => ({
|
||||
useChannelIcon: () => ref('i-test-channel'),
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params = {}) => {
|
||||
if (key === 'FILTER.CONTACT_FALLBACK') return `Contact #${params.id}`;
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('filterHelper', () => {
|
||||
describe('getCustomAttributeInputType', () => {
|
||||
it('returns date for date type', () => {
|
||||
@@ -163,64 +135,3 @@ describe('filterHelper', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const storeValues = {
|
||||
'attributes/getConversationAttributes': ref([]),
|
||||
'labels/getLabels': ref([]),
|
||||
'agents/getAgents': ref([]),
|
||||
'inboxes/getInboxes': ref([]),
|
||||
'teams/getTeams': ref([]),
|
||||
'campaigns/getAllCampaigns': ref([]),
|
||||
};
|
||||
|
||||
describe('useConversationFilterContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useMapGetter.mockImplementation(key => storeValues[key] || ref([]));
|
||||
});
|
||||
|
||||
it('exposes contact as an async searchable conversation filter', () => {
|
||||
const { filterTypes } = useConversationFilterContext();
|
||||
const contactFilter = filterTypes.value.find(
|
||||
filter => filter.attributeKey === CONVERSATION_ATTRIBUTES.CONTACT_ID
|
||||
);
|
||||
|
||||
expect(contactFilter).toMatchObject({
|
||||
attributeKey: 'contact_id',
|
||||
label: 'FILTER.ATTRIBUTES.CONTACT',
|
||||
inputType: 'asyncSearchSelect',
|
||||
dataType: 'number',
|
||||
attributeModel: 'standard',
|
||||
});
|
||||
expect(
|
||||
contactFilter.filterOperators.map(operator => operator.value)
|
||||
).toEqual(['equal_to', 'not_equal_to']);
|
||||
});
|
||||
|
||||
it('uses the existing contact search API for contact filter options', async () => {
|
||||
ContactAPI.search.mockResolvedValue({
|
||||
data: {
|
||||
payload: [
|
||||
{ id: 1, name: 'Jane Doe' },
|
||||
{ id: 2, email: 'alex@example.com' },
|
||||
{ id: 3 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { filterTypes } = useConversationFilterContext();
|
||||
const contactFilter = filterTypes.value.find(
|
||||
filter => filter.attributeKey === CONVERSATION_ATTRIBUTES.CONTACT_ID
|
||||
);
|
||||
const options = await contactFilter.searchOptions('jane');
|
||||
|
||||
expect(ContactAPI.search).toHaveBeenCalledWith('jane', 1, 'name', '', {
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
expect(options).toEqual([
|
||||
{ id: 1, name: 'Jane Doe' },
|
||||
{ id: 2, name: 'alex@example.com' },
|
||||
{ id: 3, name: 'Contact #3' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,6 @@ import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||
|
||||
const {
|
||||
options,
|
||||
asyncSearch,
|
||||
isSearching,
|
||||
disableSearch,
|
||||
disableDeselect,
|
||||
placeholderIcon,
|
||||
@@ -25,14 +23,6 @@ const {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
asyncSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSearching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -63,12 +53,6 @@ const {
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
|
||||
// the input is re-inserted on every dropdown open (v-if),
|
||||
// where the native autofocus attribute is ignored so focus it via a directive instead
|
||||
const vFocus = { mounted: el => el.focus() };
|
||||
|
||||
const { t } = useI18n();
|
||||
const selected = defineModel({
|
||||
type: Object,
|
||||
@@ -76,9 +60,7 @@ const selected = defineModel({
|
||||
});
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const searchResults = computed(() => {
|
||||
if (asyncSearch) return options;
|
||||
if (!options) return [];
|
||||
return picoSearch(options, searchTerm.value, ['name']);
|
||||
});
|
||||
@@ -95,11 +77,7 @@ const selectedItem = computed(() => {
|
||||
if (!optionToSearch) return null;
|
||||
// extract the selected item from the options array
|
||||
// this ensures that options like icon is also included
|
||||
return (
|
||||
options.find(option => option.id === optionToSearch.id) ||
|
||||
// async options may not include the selected option, fall back to it
|
||||
(asyncSearch && optionToSearch.id !== undefined ? optionToSearch : null)
|
||||
);
|
||||
return options.find(option => option.id === optionToSearch.id);
|
||||
});
|
||||
|
||||
const toggleSelected = option => {
|
||||
@@ -153,19 +131,13 @@ const toggleSelected = option => {
|
||||
<Icon class="absolute size-4 left-2 top-2" icon="i-lucide-search" />
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
v-focus
|
||||
autofocus
|
||||
class="p-1.5 pl-8 text-n-slate-11 bg-n-alpha-1 rounded-lg w-full"
|
||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||
@input="emit('search', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<DropdownSection :height="dropdownMaxHeight">
|
||||
<template v-if="isSearching">
|
||||
<DropdownItem disabled>
|
||||
{{ t('DROPDOWN_MENU.SEARCHING') }}
|
||||
</DropdownItem>
|
||||
</template>
|
||||
<template v-else-if="searchResults.length">
|
||||
<template v-if="searchResults.length">
|
||||
<DropdownItem
|
||||
v-for="option in searchResults"
|
||||
:key="option.id"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useOperators } from './operators';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import { useChannelIcon } from 'next/icon/provider';
|
||||
import { createContactSearcher } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
|
||||
import {
|
||||
buildAttributesFilterTypes,
|
||||
CONVERSATION_ATTRIBUTES,
|
||||
@@ -31,7 +30,7 @@ import languages from 'dashboard/components/widgets/conversation/advancedFilterI
|
||||
* @property {string} value - This is a proxy for the attribute key used in FilterSelect
|
||||
* @property {string} attributeName - The attribute name used to display on the UI
|
||||
* @property {string} label - This is a proxy for the attribute name used in FilterSelect
|
||||
* @property {'multiSelect'|'searchSelect'|'asyncSearchSelect'|'plainText'|'date'|'booleanSelect'} inputType - The input type for the attribute
|
||||
* @property {'multiSelect'|'searchSelect'|'plainText'|'date'|'booleanSelect'} inputType - The input type for the attribute
|
||||
* @property {FilterOption[]} [options] - The options available for the attribute if it is a multiSelect or singleSelect type
|
||||
* @property {'text'|'number'} dataType
|
||||
* @property {FilterOperator[]} filterOperators - The operators available for the attribute
|
||||
@@ -69,30 +68,6 @@ export function useConversationFilterContext() {
|
||||
getOperatorTypes,
|
||||
} = useOperators();
|
||||
|
||||
const searchContacts = createContactSearcher();
|
||||
|
||||
const contactOptionName = contact =>
|
||||
contact.name ||
|
||||
contact.email ||
|
||||
contact.phoneNumber ||
|
||||
contact.identifier ||
|
||||
t('FILTER.CONTACT_FALLBACK', { id: contact.id });
|
||||
|
||||
const searchContactOptions = async query => {
|
||||
const contacts = await searchContacts(query, {
|
||||
skipMinLength: true,
|
||||
reachableOnly: false,
|
||||
});
|
||||
|
||||
// null means the request was aborted (a newer search is in-flight)
|
||||
if (contacts === null) return null;
|
||||
|
||||
return contacts.map(contact => ({
|
||||
id: contact.id,
|
||||
name: contactOptionName(contact),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<FilterType[]>}
|
||||
*/
|
||||
@@ -183,18 +158,6 @@ export function useConversationFilterContext() {
|
||||
filterOperators: presenceOperators.value,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: CONVERSATION_ATTRIBUTES.CONTACT_ID,
|
||||
value: CONVERSATION_ATTRIBUTES.CONTACT_ID,
|
||||
attributeName: t('FILTER.ATTRIBUTES.CONTACT'),
|
||||
label: t('FILTER.ATTRIBUTES.CONTACT'),
|
||||
inputType: 'asyncSearchSelect',
|
||||
searchOptions: searchContactOptions,
|
||||
searchPlaceholder: t('FILTER.CONTACT_SEARCH_PLACEHOLDER'),
|
||||
dataType: 'number',
|
||||
filterOperators: equalityOperators.value,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
|
||||
value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
|
||||
|
||||
@@ -145,6 +145,7 @@ const allowedMenuItems = computed(() => {
|
||||
:src="currentUser.avatar_url"
|
||||
:status="currentUserAvailability"
|
||||
class="flex-shrink-0"
|
||||
rounded-full
|
||||
/>
|
||||
<div v-if="!isCollapsed" class="min-w-0">
|
||||
<div class="text-sm font-medium leading-4 truncate text-n-slate-12">
|
||||
|
||||
@@ -151,9 +151,6 @@ const activeFolder = computed(() => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const getContact = useMapGetter('contacts/getContact');
|
||||
const folderContactId = useMapGetter('customViews/getActiveFolderContactId');
|
||||
|
||||
const activeFolderName = computed(() => {
|
||||
return activeFolder.value?.name;
|
||||
});
|
||||
@@ -459,7 +456,6 @@ function setParamsForEditFolderModal() {
|
||||
inboxes: inboxesList.value,
|
||||
labels: labels.value,
|
||||
campaigns: campaigns.value,
|
||||
contacts: [getContact.value(folderContactId.value)],
|
||||
languages: languages,
|
||||
countries: countries,
|
||||
priority: [
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
CONVERSATION_EVENTS,
|
||||
CAPTAIN_EVENTS,
|
||||
} from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { MESSAGE_EDITOR_IMAGE_RESIZES } from 'dashboard/constants/editor';
|
||||
|
||||
import {
|
||||
messageSchema,
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
MessageMarkdownSerializer,
|
||||
EditorState,
|
||||
Selection,
|
||||
imageResizeView,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import {
|
||||
suggestionsPlugin,
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
insertAtCursor,
|
||||
removeSignature as removeSignatureHelper,
|
||||
scrollCursorIntoView,
|
||||
setURLWithQueryAndSize,
|
||||
getFormattingForEditor,
|
||||
getSelectionCoords,
|
||||
calculateMenuPosition,
|
||||
@@ -71,7 +72,6 @@ import {
|
||||
import { createTypingIndicator } from '@chatwoot/utils';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
@@ -93,6 +93,7 @@ const props = defineProps({
|
||||
channelType: { type: String, default: '' },
|
||||
conversationId: { type: Number, default: null },
|
||||
medium: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
focusOnMount: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
@@ -118,14 +119,6 @@ const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
const DEFAULT_FORMATTING = 'Context::Default';
|
||||
const PRIVATE_NOTE_FORMATTING = 'Context::PrivateNote';
|
||||
const MESSAGE_SIGNATURE_FORMATTING = 'Context::MessageSignature';
|
||||
const INLINE_IMAGE_PASTE_TYPES = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
const effectiveChannelType = computed(() =>
|
||||
getEffectiveChannelType(props.channelType, props.medium)
|
||||
@@ -199,8 +192,12 @@ const cannedSearchTerm = ref('');
|
||||
const variableSearchTerm = ref('');
|
||||
const emojiSearchTerm = ref('');
|
||||
const range = ref(null);
|
||||
const isImageNodeSelected = ref(false);
|
||||
const toolbarPosition = ref({ top: 0, left: 0 });
|
||||
const selectedImageNode = ref(null);
|
||||
const isTextSelected = ref(false); // Tracks text selection and prevents unnecessary re-renders on mouse selection
|
||||
const showSelectionMenu = ref(false);
|
||||
const sizes = MESSAGE_EDITOR_IMAGE_RESIZES;
|
||||
|
||||
// element ref
|
||||
const editorRoot = useTemplateRef('editorRoot');
|
||||
@@ -478,6 +475,16 @@ function removeSignature() {
|
||||
reloadState(content);
|
||||
}
|
||||
|
||||
function setToolbarPosition() {
|
||||
const editorRect = editorRoot.value.getBoundingClientRect();
|
||||
const rect = selectedImageNode.value.getBoundingClientRect();
|
||||
|
||||
toolbarPosition.value = {
|
||||
top: `${rect.top - editorRect.top - 30}px`,
|
||||
left: `${rect.left - editorRect.left - 4}px`,
|
||||
};
|
||||
}
|
||||
|
||||
function setMenubarPosition({ selection } = {}) {
|
||||
const wrapper = editorRoot.value;
|
||||
if (!selection || !wrapper) return;
|
||||
@@ -513,6 +520,30 @@ function checkSelection(editorState) {
|
||||
if (hasSelection) setMenubarPosition(editorState);
|
||||
}
|
||||
|
||||
function setURLWithQueryAndImageSize(size) {
|
||||
if (!props.showImageResizeToolbar) {
|
||||
return;
|
||||
}
|
||||
setURLWithQueryAndSize(selectedImageNode.value, size, editorView);
|
||||
isImageNodeSelected.value = false;
|
||||
}
|
||||
|
||||
function isEditorMouseFocusedOnAnImage() {
|
||||
if (!props.showImageResizeToolbar) {
|
||||
return;
|
||||
}
|
||||
selectedImageNode.value = document.querySelector(
|
||||
'img.ProseMirror-selectednode'
|
||||
);
|
||||
if (selectedImageNode.value) {
|
||||
isImageNodeSelected.value = !!selectedImageNode.value;
|
||||
// Get the position of the selected node
|
||||
setToolbarPosition();
|
||||
} else {
|
||||
isImageNodeSelected.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function emitOnChange() {
|
||||
emit('input', contentFromEditor());
|
||||
emit('update:modelValue', contentFromEditor());
|
||||
@@ -532,6 +563,21 @@ function toggleSignatureInEditor(signatureEnabled) {
|
||||
emitOnChange();
|
||||
}
|
||||
|
||||
function updateImgToolbarOnDelete() {
|
||||
// check if the selected node is present or not on keyup
|
||||
// this is needed because the user can select an image and then delete it
|
||||
// in that case, the selected node will be null and we need to hide the toolbar
|
||||
// otherwise, the toolbar will be visible even when the image is deleted and cause some errors
|
||||
if (selectedImageNode.value) {
|
||||
const hasImgSelectedNode = document.querySelector(
|
||||
'img.ProseMirror-selectednode'
|
||||
);
|
||||
if (!hasImgSelectedNode) {
|
||||
isImageNodeSelected.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEnterToSendEnabled() {
|
||||
return isEditorHotKeyEnabled('enter');
|
||||
}
|
||||
@@ -540,6 +586,17 @@ function isCmdPlusEnterToSendEnabled() {
|
||||
return isEditorHotKeyEnabled('cmd_enter');
|
||||
}
|
||||
|
||||
useKeyboardEvents({
|
||||
'Alt+KeyP': {
|
||||
action: focusEditorInputField,
|
||||
allowOnFocusedInput: false,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: focusEditorInputField,
|
||||
allowOnFocusedInput: false,
|
||||
},
|
||||
});
|
||||
|
||||
function onImageInsertInEditor(fileUrl) {
|
||||
const { tr } = editorView.state;
|
||||
|
||||
@@ -560,11 +617,7 @@ async function uploadImageToStorage(file) {
|
||||
onImageInsertInEditor(fileUrl);
|
||||
}
|
||||
useAlert(
|
||||
props.channelType === MESSAGE_SIGNATURE_FORMATTING
|
||||
? t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
|
||||
)
|
||||
: t('CONVERSATION.REPLYBOX.IMAGE_UPLOAD_SUCCESS')
|
||||
t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
@@ -573,8 +626,8 @@ async function uploadImageToStorage(file) {
|
||||
}
|
||||
}
|
||||
|
||||
function uploadImageIfWithinSizeLimit(file) {
|
||||
if (!file) return;
|
||||
function onFileChange() {
|
||||
const file = imageUpload.value.files[0];
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
uploadImageToStorage(file);
|
||||
} else {
|
||||
@@ -587,61 +640,10 @@ function uploadImageIfWithinSizeLimit(file) {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
imageUpload.value = '';
|
||||
}
|
||||
|
||||
function onFileChange() {
|
||||
const input = imageUpload.value;
|
||||
uploadImageIfWithinSizeLimit(input.files[0]);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
const allowsInlineImagePaste = computed(
|
||||
() =>
|
||||
!props.isPrivate &&
|
||||
(props.channelType === INBOX_TYPES.EMAIL ||
|
||||
props.channelType === INBOX_TYPES.WEB)
|
||||
);
|
||||
|
||||
// Shift+Cmd/Ctrl+V on email/website: upload a clipboard image inline. This
|
||||
// gesture's native paste event carries no image, so clipboard.read() is the
|
||||
// only way to get the bytes. No preventDefault: text still pastes natively.
|
||||
async function pasteInlineImageFromClipboard() {
|
||||
if (!editorView?.hasFocus()) return;
|
||||
if (!allowsInlineImagePaste.value || !navigator.clipboard?.read) return;
|
||||
try {
|
||||
const items = await navigator.clipboard.read();
|
||||
const imageItem = items.find(item =>
|
||||
item.types.some(type => INLINE_IMAGE_PASTE_TYPES.includes(type))
|
||||
);
|
||||
if (!imageItem) return;
|
||||
const imageType = imageItem.types.find(type =>
|
||||
INLINE_IMAGE_PASTE_TYPES.includes(type)
|
||||
);
|
||||
const blob = await imageItem.getType(imageType);
|
||||
uploadImageIfWithinSizeLimit(
|
||||
new File([blob], 'pasted-image', { type: imageType })
|
||||
);
|
||||
} catch (error) {
|
||||
// clipboard-read denied/unfocused (NotAllowedError): image can't be read.
|
||||
// Text paste is unaffected — ProseMirror handles it from the native event.
|
||||
}
|
||||
}
|
||||
|
||||
useKeyboardEvents({
|
||||
'Alt+KeyP': {
|
||||
action: focusEditorInputField,
|
||||
allowOnFocusedInput: false,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: focusEditorInputField,
|
||||
allowOnFocusedInput: false,
|
||||
},
|
||||
'$mod+Shift+KeyV': {
|
||||
action: pasteInlineImageFromClipboard,
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
});
|
||||
|
||||
function handleLineBreakWhenEnterToSendEnabled(event) {
|
||||
if (
|
||||
hasPressedEnterAndNotCmdOrShift(event) &&
|
||||
@@ -734,9 +736,6 @@ function createEditorView() {
|
||||
editorView = new EditorView(editor.value, {
|
||||
state: state,
|
||||
editable: () => !props.disabled,
|
||||
nodeViews: {
|
||||
image: imageResizeView,
|
||||
},
|
||||
dispatchTransaction: tx => {
|
||||
state = state.apply(tx);
|
||||
editorView.updateState(state);
|
||||
@@ -749,10 +748,12 @@ function createEditorView() {
|
||||
keyup: () => {
|
||||
if (!props.disabled) {
|
||||
typingIndicator.start();
|
||||
updateImgToolbarOnDelete();
|
||||
}
|
||||
},
|
||||
keydown: (view, event) => !props.disabled && onKeydown(event),
|
||||
focus: () => !props.disabled && emit('focus'),
|
||||
click: () => !props.disabled && isEditorMouseFocusedOnAnImage(),
|
||||
blur: () => {
|
||||
if (props.disabled) return;
|
||||
typingIndicator.stop();
|
||||
@@ -917,6 +918,23 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div ref="editor" />
|
||||
<div
|
||||
v-show="isImageNodeSelected && showImageResizeToolbar"
|
||||
class="absolute shadow-md rounded-[6px] flex gap-1 py-1 px-1 bg-n-solid-3 outline outline-1 outline-n-weak text-n-slate-12"
|
||||
:style="{
|
||||
top: toolbarPosition.top,
|
||||
left: toolbarPosition.left,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-for="size in sizes"
|
||||
:key="size.name"
|
||||
class="text-xs font-medium rounded-[4px] outline outline-1 outline-n-strong px-1.5 py-0.5 hover:bg-n-slate-5"
|
||||
@click="setURLWithQueryAndImageSize(size)"
|
||||
>
|
||||
{{ size.name }}
|
||||
</button>
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -979,6 +997,10 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
@apply text-n-slate-11;
|
||||
}
|
||||
}
|
||||
|
||||
ol li {
|
||||
@apply list-item list-decimal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ const copyConversationId = async () => {
|
||||
:size="32"
|
||||
:status="currentContact.availability_status"
|
||||
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"
|
||||
|
||||
@@ -375,10 +375,10 @@ export default {
|
||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
},
|
||||
audioRecordFormat() {
|
||||
if (this.isAWhatsAppCloudChannel) {
|
||||
if (this.isAWhatsAppChannel) {
|
||||
return AUDIO_FORMATS.OGG;
|
||||
}
|
||||
if (this.isAWhatsAppChannel || this.isATelegramChannel) {
|
||||
if (this.isATelegramChannel) {
|
||||
return AUDIO_FORMATS.MP3;
|
||||
}
|
||||
if (this.isAPIInbox) {
|
||||
|
||||
@@ -46,14 +46,6 @@ const filterTypes = [
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'contact_id',
|
||||
attributeI18nKey: 'CONTACT',
|
||||
inputType: 'search_select',
|
||||
dataType: 'number',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'display_id',
|
||||
attributeI18nKey: 'CONVERSATION_IDENTIFIER',
|
||||
@@ -141,10 +133,6 @@ export const filterAttributeGroups = [
|
||||
key: 'team_id',
|
||||
i18nKey: 'TEAM_NAME',
|
||||
},
|
||||
{
|
||||
key: 'contact_id',
|
||||
i18nKey: 'CONTACT',
|
||||
},
|
||||
{
|
||||
key: 'display_id',
|
||||
i18nKey: 'CONVERSATION_IDENTIFIER',
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import { useFacebookPageConnect } from '../useFacebookPageConnect';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import ChannelApi from 'dashboard/api/channels';
|
||||
import { setupFacebookSdk } from 'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils';
|
||||
|
||||
vi.mock('dashboard/composables/store', () => ({ useMapGetter: vi.fn() }));
|
||||
vi.mock('dashboard/api/channels', () => ({
|
||||
default: { fetchFacebookPages: vi.fn() },
|
||||
}));
|
||||
vi.mock(
|
||||
'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils',
|
||||
() => ({ setupFacebookSdk: vi.fn() })
|
||||
);
|
||||
|
||||
const flushPromises = () =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
const createDeferred = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const ACCOUNT_ID = 42;
|
||||
const PAGES = [
|
||||
{ id: 'p1', name: 'Page One', access_token: 'page-token-1' },
|
||||
{ id: 'p2', name: 'Page Two', access_token: 'page-token-2', exists: true },
|
||||
];
|
||||
|
||||
const pagesResponse = {
|
||||
data: { data: { page_details: PAGES, user_access_token: 'long-token' } },
|
||||
};
|
||||
|
||||
// FB.login invokes its callback with the given response.
|
||||
const stubLogin = response => {
|
||||
window.FB = { login: vi.fn(callback => callback(response)) };
|
||||
};
|
||||
|
||||
const connected = {
|
||||
status: 'connected',
|
||||
authResponse: { accessToken: 'user-token' },
|
||||
};
|
||||
|
||||
describe('useFacebookPageConnect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.chatwootConfig = { fbAppId: 'fb-app', fbApiVersion: 'v22.0' };
|
||||
useMapGetter.mockReturnValue({ value: ACCOUNT_ID });
|
||||
setupFacebookSdk.mockResolvedValue();
|
||||
ChannelApi.fetchFacebookPages.mockResolvedValue(pagesResponse);
|
||||
stubLogin(connected);
|
||||
});
|
||||
|
||||
it('resolves the user token and pages on a connected login', async () => {
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
await expect(loginAndFetchPages()).resolves.toEqual({
|
||||
userAccessToken: 'long-token',
|
||||
pages: PAGES,
|
||||
});
|
||||
expect(setupFacebookSdk).toHaveBeenCalledWith('fb-app', 'v22.0');
|
||||
expect(ChannelApi.fetchFacebookPages).toHaveBeenCalledWith(
|
||||
'user-token',
|
||||
ACCOUNT_ID
|
||||
);
|
||||
expect(window.FB.login).toHaveBeenCalledWith(expect.any(Function), {
|
||||
scope: expect.stringContaining('pages_show_list'),
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves null when the user is not authorized', async () => {
|
||||
stubLogin({ status: 'not_authorized' });
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
await expect(loginAndFetchPages()).resolves.toBeNull();
|
||||
expect(ChannelApi.fetchFacebookPages).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves null on an unknown login status', async () => {
|
||||
stubLogin({ status: 'unknown' });
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
await expect(loginAndFetchPages()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('rejects when fetching pages fails', async () => {
|
||||
ChannelApi.fetchFacebookPages.mockRejectedValue(new Error('fetch failed'));
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
await expect(loginAndFetchPages()).rejects.toThrow('fetch failed');
|
||||
});
|
||||
|
||||
it('rejects when the SDK fails to load', async () => {
|
||||
const error = new Error('script load failed');
|
||||
error.name = 'ScriptLoaderError';
|
||||
setupFacebookSdk.mockRejectedValue(error);
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
await expect(loginAndFetchPages()).rejects.toThrow('script load failed');
|
||||
});
|
||||
|
||||
it('ignores a second call while a run is in flight', async () => {
|
||||
const pending = createDeferred();
|
||||
ChannelApi.fetchFacebookPages.mockReturnValue(pending.promise);
|
||||
|
||||
const { loginAndFetchPages } = useFacebookPageConnect();
|
||||
const first = loginAndFetchPages();
|
||||
const second = loginAndFetchPages();
|
||||
|
||||
await expect(second).resolves.toBeNull();
|
||||
|
||||
pending.resolve(pagesResponse);
|
||||
await first;
|
||||
expect(window.FB.login).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toggles isAuthenticating across a run', async () => {
|
||||
const pending = createDeferred();
|
||||
ChannelApi.fetchFacebookPages.mockReturnValue(pending.promise);
|
||||
|
||||
const { isAuthenticating, loginAndFetchPages } = useFacebookPageConnect();
|
||||
expect(isAuthenticating.value).toBe(false);
|
||||
|
||||
const result = loginAndFetchPages();
|
||||
await flushPromises();
|
||||
expect(isAuthenticating.value).toBe(true);
|
||||
|
||||
pending.resolve(pagesResponse);
|
||||
await result;
|
||||
expect(isAuthenticating.value).toBe(false);
|
||||
});
|
||||
|
||||
it('preloads the SDK once and reuses it for login', async () => {
|
||||
const { preloadSdk, loginAndFetchPages } = useFacebookPageConnect();
|
||||
|
||||
preloadSdk();
|
||||
preloadSdk();
|
||||
await loginAndFetchPages();
|
||||
|
||||
expect(setupFacebookSdk).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,208 +0,0 @@
|
||||
import { useWhatsappEmbeddedSignup } from '../useWhatsappEmbeddedSignup';
|
||||
import {
|
||||
setupFacebookSdk,
|
||||
initWhatsAppEmbeddedSignup,
|
||||
createMessageHandler,
|
||||
} from 'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils';
|
||||
|
||||
vi.mock(
|
||||
'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils',
|
||||
() => ({
|
||||
setupFacebookSdk: vi.fn(),
|
||||
initWhatsAppEmbeddedSignup: vi.fn(),
|
||||
createMessageHandler: vi.fn(),
|
||||
isValidBusinessData: vi.fn(data =>
|
||||
Boolean(data && data.business_id && data.waba_id)
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
const flushPromises = () =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
const createDeferred = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const VALID_BUSINESS = {
|
||||
business_id: 'biz-1',
|
||||
waba_id: 'waba-1',
|
||||
phone_number_id: 'phone-1',
|
||||
};
|
||||
|
||||
describe('useWhatsappEmbeddedSignup', () => {
|
||||
// The mocked createMessageHandler captures the callback the composable
|
||||
// registers, so tests can simulate Meta's WA_EMBEDDED_SIGNUP postMessages
|
||||
// directly without the window-event + origin plumbing (that is covered by
|
||||
// the utils' own tests).
|
||||
let signupCallback;
|
||||
let registeredListener;
|
||||
|
||||
const emit = data => signupCallback(data);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
window.chatwootConfig = {
|
||||
whatsappAppId: 'app-id',
|
||||
whatsappConfigurationId: 'config-id',
|
||||
whatsappApiVersion: 'v22.0',
|
||||
};
|
||||
|
||||
setupFacebookSdk.mockResolvedValue();
|
||||
createMessageHandler.mockImplementation(callback => {
|
||||
signupCallback = callback;
|
||||
registeredListener = () => {};
|
||||
return registeredListener;
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves credentials when the auth code arrives before the business data', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockResolvedValue('auth-code');
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
await flushPromises(); // SDK setup + FB.login resolve the code first
|
||||
emit({ event: 'FINISH', data: VALID_BUSINESS });
|
||||
|
||||
await expect(result).resolves.toEqual({
|
||||
code: 'auth-code',
|
||||
business_id: 'biz-1',
|
||||
waba_id: 'waba-1',
|
||||
phone_number_id: 'phone-1',
|
||||
});
|
||||
expect(setupFacebookSdk).toHaveBeenCalledWith('app-id', 'v22.0');
|
||||
expect(initWhatsAppEmbeddedSignup).toHaveBeenCalledWith('config-id');
|
||||
});
|
||||
|
||||
it('resolves credentials when the business data arrives before the auth code', async () => {
|
||||
const code = createDeferred();
|
||||
initWhatsAppEmbeddedSignup.mockReturnValue(code.promise);
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
// Business data lands first, while FB.login is still pending.
|
||||
emit({
|
||||
event: 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING',
|
||||
data: VALID_BUSINESS,
|
||||
});
|
||||
code.resolve('late-code');
|
||||
|
||||
await expect(result).resolves.toEqual({
|
||||
code: 'late-code',
|
||||
business_id: 'biz-1',
|
||||
waba_id: 'waba-1',
|
||||
phone_number_id: 'phone-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults phone_number_id to an empty string when absent', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockResolvedValue('auth-code');
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
await flushPromises();
|
||||
emit({
|
||||
event: 'FINISH',
|
||||
data: { business_id: 'biz-1', waba_id: 'waba-1' },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toMatchObject({ phone_number_id: '' });
|
||||
});
|
||||
|
||||
it('resolves null when FB.login is cancelled', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockRejectedValue(new Error('Login cancelled'));
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
|
||||
await expect(runEmbeddedSignup()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('resolves null on a CANCEL event', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockReturnValue(createDeferred().promise);
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
emit({ event: 'CANCEL' });
|
||||
|
||||
await expect(result).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('rejects with the Meta error message on an error event', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockReturnValue(createDeferred().promise);
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
emit({ event: 'error', error_message: 'WABA not eligible' });
|
||||
|
||||
await expect(result).rejects.toThrow('WABA not eligible');
|
||||
});
|
||||
|
||||
it('rejects when the business data is invalid', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockReturnValue(createDeferred().promise);
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const result = runEmbeddedSignup();
|
||||
|
||||
emit({ event: 'FINISH', data: { business_id: 'biz-1' } }); // no waba_id
|
||||
|
||||
await expect(result).rejects.toThrow('Invalid business data');
|
||||
});
|
||||
|
||||
it('rejects when the SDK or login fails for a non-cancel reason', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockRejectedValue(new Error('popup blocked'));
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
|
||||
await expect(runEmbeddedSignup()).rejects.toThrow('popup blocked');
|
||||
});
|
||||
|
||||
it('ignores a second call while a run is in flight', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockResolvedValue('auth-code');
|
||||
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const first = runEmbeddedSignup();
|
||||
const second = runEmbeddedSignup();
|
||||
|
||||
await expect(second).resolves.toBeNull();
|
||||
|
||||
// Let the first run finish so it doesn't leak into the next test.
|
||||
await flushPromises();
|
||||
emit({ event: 'FINISH', data: VALID_BUSINESS });
|
||||
await first;
|
||||
|
||||
expect(setupFacebookSdk).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toggles isAuthenticating and removes the listener once settled', async () => {
|
||||
initWhatsAppEmbeddedSignup.mockResolvedValue('auth-code');
|
||||
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { isAuthenticating, runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
expect(isAuthenticating.value).toBe(false);
|
||||
|
||||
const result = runEmbeddedSignup();
|
||||
expect(isAuthenticating.value).toBe(true);
|
||||
|
||||
await flushPromises();
|
||||
emit({ event: 'FINISH', data: VALID_BUSINESS });
|
||||
await result;
|
||||
|
||||
expect(isAuthenticating.value).toBe(false);
|
||||
expect(removeSpy).toHaveBeenCalledWith('message', registeredListener);
|
||||
removeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import ChannelApi from 'dashboard/api/channels';
|
||||
import { setupFacebookSdk } from 'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils';
|
||||
|
||||
// Page-management + messaging scopes required to list pages and create a
|
||||
// Channel::FacebookPage inbox (mirrors the standalone settings flow).
|
||||
const FB_PAGE_SCOPES =
|
||||
'pages_manage_metadata,business_management,pages_messaging,instagram_basic,pages_show_list,pages_read_engagement,instagram_manage_messages';
|
||||
|
||||
// Headless half of the Facebook Page connect flow: load the Meta SDK, run
|
||||
// FB.login for page scopes, and fetch the user's pages. The caller owns the
|
||||
// page-picker UI and the channel creation, because choosing a page is an
|
||||
// interactive step (a user can manage several pages).
|
||||
//
|
||||
// Split into preloadSdk() + loginAndFetchPages() for popup safety: FB.login
|
||||
// opens a popup and needs the click's transient activation. Preloading the SDK
|
||||
// when the picker opens means the click-time `await` resolves within that
|
||||
// activation window; a cold load resolves on the script's `load` task seconds
|
||||
// later, after activation has expired, and the popup gets blocked.
|
||||
export function useFacebookPageConnect() {
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
const isAuthenticating = ref(false);
|
||||
|
||||
let sdkSetupPromise = null;
|
||||
|
||||
// Idempotent — call this when the picker UI opens. A failed load clears the
|
||||
// cache so a later attempt can retry instead of being stuck on a rejection.
|
||||
const preloadSdk = () => {
|
||||
if (!sdkSetupPromise) {
|
||||
sdkSetupPromise = setupFacebookSdk(
|
||||
window.chatwootConfig?.fbAppId,
|
||||
window.chatwootConfig?.fbApiVersion
|
||||
).catch(error => {
|
||||
sdkSetupPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return sdkSetupPromise;
|
||||
};
|
||||
|
||||
// FB.login never rejects; resolve the user access token on success and null
|
||||
// for any other status (closed popup, not_authorized, unknown).
|
||||
const login = () =>
|
||||
new Promise(resolve => {
|
||||
window.FB.login(
|
||||
response => {
|
||||
resolve(
|
||||
response.status === 'connected'
|
||||
? response.authResponse?.accessToken || null
|
||||
: null
|
||||
);
|
||||
},
|
||||
{ scope: FB_PAGE_SCOPES }
|
||||
);
|
||||
});
|
||||
|
||||
// Resolves { userAccessToken, pages } on success, null when the user cancels,
|
||||
// and rejects on SDK-load or page-fetch failure (the caller maps it to UI).
|
||||
const loginAndFetchPages = async () => {
|
||||
if (isAuthenticating.value) return null;
|
||||
isAuthenticating.value = true;
|
||||
try {
|
||||
await preloadSdk();
|
||||
const token = await login();
|
||||
if (!token) return null;
|
||||
|
||||
const response = await ChannelApi.fetchFacebookPages(
|
||||
token,
|
||||
accountId.value
|
||||
);
|
||||
const { page_details: pages, user_access_token: userAccessToken } =
|
||||
response.data.data;
|
||||
return { userAccessToken, pages };
|
||||
} finally {
|
||||
isAuthenticating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { isAuthenticating, preloadSdk, loginAndFetchPages };
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
setupFacebookSdk,
|
||||
initWhatsAppEmbeddedSignup,
|
||||
createMessageHandler,
|
||||
isValidBusinessData,
|
||||
} from 'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils';
|
||||
|
||||
// Drives Meta's WhatsApp embedded-signup popup (Facebook JS SDK). FB.login()
|
||||
// resolves an auth `code` while the WABA identifiers (waba_id, phone_number_id)
|
||||
// arrive separately over a postMessage event — order isn't guaranteed, so we
|
||||
// hold both and resolve once both are present.
|
||||
//
|
||||
// `runEmbeddedSignup` returns the signup credentials; the caller exchanges them
|
||||
// for an inbox via `inboxes/createWhatsAppEmbeddedSignup` and owns its own UX
|
||||
// (alerts, navigation, etc). Resolves `null` when the user cancels the popup;
|
||||
// rejects on SDK load or signup errors. The window listener is scoped to a
|
||||
// single run, so this is safe to call from anywhere without lifecycle wiring.
|
||||
export function useWhatsappEmbeddedSignup() {
|
||||
const isAuthenticating = ref(false);
|
||||
|
||||
const runEmbeddedSignup = () => {
|
||||
if (isAuthenticating.value) return Promise.resolve(null);
|
||||
isAuthenticating.value = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let authCode = null;
|
||||
let businessData = null;
|
||||
let settled = false;
|
||||
let messageHandler;
|
||||
|
||||
const settle = (fn, value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
window.removeEventListener('message', messageHandler);
|
||||
isAuthenticating.value = false;
|
||||
fn(value);
|
||||
};
|
||||
|
||||
// Both the auth code and the business data arrive asynchronously and in
|
||||
// no fixed order; only resolve once we're holding both.
|
||||
const resolveIfReady = () => {
|
||||
if (!authCode || !businessData) return;
|
||||
settle(resolve, {
|
||||
code: authCode,
|
||||
business_id: businessData.business_id,
|
||||
waba_id: businessData.waba_id,
|
||||
phone_number_id: businessData.phone_number_id || '',
|
||||
});
|
||||
};
|
||||
|
||||
messageHandler = createMessageHandler(data => {
|
||||
if (
|
||||
data.event === 'FINISH' ||
|
||||
data.event === 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING'
|
||||
) {
|
||||
if (!isValidBusinessData(data.data)) {
|
||||
settle(reject, new Error('Invalid business data'));
|
||||
return;
|
||||
}
|
||||
businessData = data.data;
|
||||
resolveIfReady();
|
||||
} else if (data.event === 'CANCEL') {
|
||||
settle(resolve, null);
|
||||
} else if (data.event === 'error') {
|
||||
settle(reject, new Error(data.error_message || 'Signup error'));
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await setupFacebookSdk(
|
||||
window.chatwootConfig?.whatsappAppId,
|
||||
window.chatwootConfig?.whatsappApiVersion
|
||||
);
|
||||
authCode = await initWhatsAppEmbeddedSignup(
|
||||
window.chatwootConfig?.whatsappConfigurationId
|
||||
);
|
||||
resolveIfReady();
|
||||
} catch (error) {
|
||||
// FB.login() rejects with 'Login cancelled' when the user dismisses
|
||||
// the popup — treat it as a cancel rather than an error.
|
||||
if (error.message === 'Login cancelled') {
|
||||
settle(resolve, null);
|
||||
} else {
|
||||
settle(reject, error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
};
|
||||
|
||||
return { isAuthenticating, runEmbeddedSignup };
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export const FORMATTING = {
|
||||
'link',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'imageUpload',
|
||||
'undo',
|
||||
'redo',
|
||||
],
|
||||
@@ -31,7 +30,6 @@ export const FORMATTING = {
|
||||
'strike',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'imageUpload',
|
||||
'undo',
|
||||
'redo',
|
||||
],
|
||||
@@ -265,3 +263,23 @@ export const MARKDOWN_PATTERNS = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Editor image resize options for Message Editor
|
||||
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
||||
{
|
||||
name: 'Small',
|
||||
height: '24px',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
height: '48px',
|
||||
},
|
||||
{
|
||||
name: 'Large',
|
||||
height: '72px',
|
||||
},
|
||||
{
|
||||
name: 'Original Size',
|
||||
height: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,13 +37,6 @@ export const getValuesName = (values, list, idKey, nameKey) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getValuesForContact = (values, contacts) => ({
|
||||
id: values[0],
|
||||
name:
|
||||
contacts?.find(contact => contact.id === values[0])?.name ||
|
||||
`Contact #${values[0]}`,
|
||||
});
|
||||
|
||||
export const getValuesForStatus = values => {
|
||||
return values.map(value => ({ id: value, name: value }));
|
||||
};
|
||||
@@ -91,7 +84,6 @@ export const getValuesForFilter = (filter, params) => {
|
||||
campaigns,
|
||||
labels,
|
||||
priority,
|
||||
contacts,
|
||||
} = params;
|
||||
switch (attribute_key) {
|
||||
case 'status':
|
||||
@@ -102,8 +94,6 @@ export const getValuesForFilter = (filter, params) => {
|
||||
return getValuesName(values, inboxes, 'id', 'name');
|
||||
case 'team_id':
|
||||
return getValuesName(values, teams, 'id', 'name');
|
||||
case 'contact_id':
|
||||
return getValuesForContact(values, contacts);
|
||||
case 'campaign_id':
|
||||
return getValuesName(values, campaigns, 'id', 'title');
|
||||
case 'labels':
|
||||
|
||||
@@ -379,6 +379,31 @@ export const findNodeToInsertImage = (editorState, fileUrl) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set URL with query and size.
|
||||
*
|
||||
* @param {Object} selectedImageNode - The current selected node.
|
||||
* @param {Object} size - The size to set.
|
||||
* @param {Object} editorView - The editor view.
|
||||
*/
|
||||
export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
||||
if (selectedImageNode) {
|
||||
// Create and apply the transaction
|
||||
const tr = editorView.state.tr.setNodeMarkup(
|
||||
editorView.state.selection.from,
|
||||
null,
|
||||
{
|
||||
src: selectedImageNode.src,
|
||||
height: size.height,
|
||||
}
|
||||
);
|
||||
|
||||
if (tr.docChanged) {
|
||||
editorView.dispatch(tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips unsupported markdown formatting from content based on the editor schema.
|
||||
* This ensures canned responses with rich formatting can be inserted into channels
|
||||
|
||||
@@ -159,13 +159,6 @@ export const LOCALE_MENU_ITEMS = {
|
||||
value: 'publish',
|
||||
icon: 'i-lucide-eye',
|
||||
},
|
||||
customizeContent: {
|
||||
label:
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.CUSTOMIZE_CONTENT',
|
||||
action: 'customize-content',
|
||||
value: 'customize-content',
|
||||
icon: 'i-lucide-pencil',
|
||||
},
|
||||
delete: {
|
||||
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE',
|
||||
action: 'delete',
|
||||
@@ -179,28 +172,20 @@ const disableLocaleMenuItems = menuItems =>
|
||||
|
||||
export const buildLocaleMenuItems = ({ isDefault, isDraft }) => {
|
||||
if (isDefault) {
|
||||
return [
|
||||
...disableLocaleMenuItems([
|
||||
LOCALE_MENU_ITEMS.makeDefault,
|
||||
LOCALE_MENU_ITEMS.moveToDraft,
|
||||
]),
|
||||
LOCALE_MENU_ITEMS.customizeContent,
|
||||
...disableLocaleMenuItems([LOCALE_MENU_ITEMS.delete]),
|
||||
];
|
||||
return disableLocaleMenuItems([
|
||||
LOCALE_MENU_ITEMS.makeDefault,
|
||||
LOCALE_MENU_ITEMS.moveToDraft,
|
||||
LOCALE_MENU_ITEMS.delete,
|
||||
]);
|
||||
}
|
||||
|
||||
if (isDraft) {
|
||||
return [
|
||||
LOCALE_MENU_ITEMS.publishLocale,
|
||||
LOCALE_MENU_ITEMS.customizeContent,
|
||||
LOCALE_MENU_ITEMS.delete,
|
||||
];
|
||||
return [LOCALE_MENU_ITEMS.publishLocale, LOCALE_MENU_ITEMS.delete];
|
||||
}
|
||||
|
||||
return [
|
||||
LOCALE_MENU_ITEMS.makeDefault,
|
||||
LOCALE_MENU_ITEMS.moveToDraft,
|
||||
LOCALE_MENU_ITEMS.customizeContent,
|
||||
LOCALE_MENU_ITEMS.delete,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -273,41 +273,6 @@ describe('customViewsHelper', () => {
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual('1');
|
||||
});
|
||||
|
||||
it('returns contact name for contact filters when contact is available', () => {
|
||||
const filter = {
|
||||
attribute_key: 'contact_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [123],
|
||||
};
|
||||
const params = {
|
||||
contacts: [{ id: 123, name: 'John Doe' }],
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
};
|
||||
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 123,
|
||||
name: 'John Doe',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fallback contact display value when contact is not available', () => {
|
||||
const filter = {
|
||||
attribute_key: 'contact_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [123],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
};
|
||||
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 123,
|
||||
name: 'Contact #123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateCustomAttributesInputType', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
insertAtCursor,
|
||||
removeSignature,
|
||||
replaceSignature,
|
||||
setURLWithQueryAndSize,
|
||||
stripInlineBase64Images,
|
||||
stripUnsupportedFormatting,
|
||||
stripUnsupportedMarkdown,
|
||||
@@ -652,6 +653,71 @@ describe('findNodeToInsertImage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setURLWithQueryAndSize', () => {
|
||||
let selectedNode;
|
||||
let editorView;
|
||||
|
||||
beforeEach(() => {
|
||||
selectedNode = {
|
||||
setAttribute: vi.fn(),
|
||||
};
|
||||
|
||||
const tr = {
|
||||
setNodeMarkup: vi.fn().mockReturnValue({
|
||||
docChanged: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const state = {
|
||||
selection: { from: 0 },
|
||||
tr,
|
||||
};
|
||||
|
||||
editorView = {
|
||||
state,
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('updates the URL with the given size and updates the editor view', () => {
|
||||
const size = { height: '20px' };
|
||||
|
||||
setURLWithQueryAndSize(selectedNode, size, editorView);
|
||||
|
||||
// Check if the editor view is updated
|
||||
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates the URL with the given size and updates the editor view with original size', () => {
|
||||
const size = { height: 'auto' };
|
||||
|
||||
setURLWithQueryAndSize(selectedNode, size, editorView);
|
||||
|
||||
// Check if the editor view is updated
|
||||
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not update the editor view if the document has not changed', () => {
|
||||
editorView.state.tr.setNodeMarkup = vi.fn().mockReturnValue({
|
||||
docChanged: false,
|
||||
});
|
||||
|
||||
const size = { height: '20px' };
|
||||
|
||||
setURLWithQueryAndSize(selectedNode, size, editorView);
|
||||
|
||||
// Check if the editor view dispatch was not called
|
||||
expect(editorView.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not perform any operations if selectedNode is not provided', () => {
|
||||
setURLWithQueryAndSize(null, { height: '20px' }, editorView);
|
||||
|
||||
// Ensure the dispatch method wasn't called
|
||||
expect(editorView.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentNode', () => {
|
||||
let mockEditorView;
|
||||
|
||||
|
||||
@@ -64,25 +64,4 @@ describe('#filterQueryGenerator', () => {
|
||||
filterQueryGenerator(testData).payload.every(i => Array.isArray(i.values))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('serializes a selected contact object to contact id', () => {
|
||||
const result = filterQueryGenerator([
|
||||
{
|
||||
attribute_key: 'contact_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: { id: 123, name: 'Jane Doe' },
|
||||
query_operator: 'and',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'contact_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [123],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,40 +74,37 @@ describe('PortalHelper', () => {
|
||||
});
|
||||
|
||||
describe('buildLocaleMenuItems', () => {
|
||||
it('disables other actions but keeps customize enabled for the default locale', () => {
|
||||
const items = buildLocaleMenuItems({ isDefault: true, isDraft: false });
|
||||
const customize = items.find(item => item.action === 'customize-content');
|
||||
|
||||
expect(customize).toBeTruthy();
|
||||
expect(customize.disabled).toBeFalsy();
|
||||
it('returns disabled actions for the default locale', () => {
|
||||
expect(
|
||||
items
|
||||
.filter(item => item.action !== 'customize-content')
|
||||
.every(item => item.disabled)
|
||||
).toBe(true);
|
||||
buildLocaleMenuItems({
|
||||
isDefault: true,
|
||||
isDraft: false,
|
||||
})
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ action: 'change-default', disabled: true }),
|
||||
expect.objectContaining({ action: 'move-to-draft', disabled: true }),
|
||||
expect.objectContaining({ action: 'delete', disabled: true }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('returns publish, customize, and delete actions for draft locales', () => {
|
||||
it('returns publish and delete actions for draft locales', () => {
|
||||
expect(
|
||||
buildLocaleMenuItems({
|
||||
isDefault: false,
|
||||
isDraft: true,
|
||||
}).map(({ action }) => action)
|
||||
).toEqual(['publish-locale', 'customize-content', 'delete']);
|
||||
).toEqual(['publish-locale', 'delete']);
|
||||
});
|
||||
|
||||
it('returns default, draft, customize, and delete actions for live locales', () => {
|
||||
it('returns default, draft, and delete actions for live locales', () => {
|
||||
expect(
|
||||
buildLocaleMenuItems({
|
||||
isDefault: false,
|
||||
isDraft: false,
|
||||
}).map(({ action }) => action)
|
||||
).toEqual([
|
||||
'change-default',
|
||||
'move-to-draft',
|
||||
'customize-content',
|
||||
'delete',
|
||||
]);
|
||||
).toEqual(['change-default', 'move-to-draft', 'delete']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "እውቂያዎችን ይፈልጉ",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "እውቂያ",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"IMAGE_UPLOAD_SUCCESS": "ምስል በተሳካ ሁኔታ ተሰብስቧል",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "ኮፒሎት እየሰማራ ነው",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "እንደ ነባሪ አድርግ",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "ሰርዝ"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "የገፅ ርዕስ"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "የራስጌ ጽሑፍ"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "አዲስ ቋንቋ አክል",
|
||||
"DESCRIPTION": "ይህ ሰነድ በሚጻፍበት ቋንቋ ይምረጡ። ይህ ወደ የትርጉም ዝርዝርዎ ይጨምራል እና በኋላ ተጨማሪ ማክሰኞ ይችላሉ።.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "أو"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "أدخل القيمة",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "يساوي",
|
||||
"not_equal_to": "لا يساوي",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "اسم المكلَّف",
|
||||
"INBOX_NAME": "اسم صندوق الوارد",
|
||||
"TEAM_NAME": "اسم الفريق",
|
||||
"CONTACT": "جهات الاتصال",
|
||||
"CONVERSATION_IDENTIFIER": "معرف المحادثة",
|
||||
"CAMPAIGN_NAME": "اسم الحملة",
|
||||
"LABELS": "الوسوم",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "تعذر فتح الصوت",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "اسحب و أسقط هنا للإرفاق",
|
||||
"IMAGE_UPLOAD_SUCCESS": "تم رفع الصورة بنجاح",
|
||||
"START_AUDIO_RECORDING": "بدء التسجيل الصوتي",
|
||||
"STOP_AUDIO_RECORDING": "إيقاف التسجيل الصوتي",
|
||||
"COPILOT_THINKING": "Copilot يفكر",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "حذف"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "الاسم"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "إضافة لغة جديدة",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Əlaqələrdə axtarış",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Əlaqə",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Səsi açmaq mümkün olmadı",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Qoşmaq üçün buraya sürükləyin və buraxın",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Şəkil uğurla yükləndi",
|
||||
"START_AUDIO_RECORDING": "Səs yazısını başla",
|
||||
"STOP_AUDIO_RECORDING": "Səs yazısını dayandırın",
|
||||
"COPILOT_THINKING": "Copilot düşünür",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Delete"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "ИЛИ"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Равно на",
|
||||
"not_equal_to": "Различно от",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Етикети",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot мисли",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Изтрий"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Име"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "যোগাযোগ অনুসন্ধান করুন",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "যোগাযোগ",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "অডিও খোলা যায়নি",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "সংযুক্ত করতে এখানে ড্র্যাগ ও ড্রপ করুন",
|
||||
"IMAGE_UPLOAD_SUCCESS": "ছবি সফলভাবে আপলোড হয়েছে",
|
||||
"START_AUDIO_RECORDING": "অডিও রেকর্ডিং শুরু করুন",
|
||||
"STOP_AUDIO_RECORDING": "অডিও রেকর্ডিং বন্ধ করুন",
|
||||
"COPILOT_THINKING": "Copilot ভাবছে",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "ডিফল্ট করুন",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "মুছে ফেলুন"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "নাম"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "পৃষ্ঠার শিরোনাম"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "হেডার টেক্সট"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "নতুন লোকেল যোগ করুন",
|
||||
"DESCRIPTION": "এই নিবন্ধটি যে ভাষায় লেখা হবে তা নির্বাচন করুন. এটি আপনার অনুবাদ তালিকায় যোগ হবে, এবং পরে আপনি আরও ভাষা যোগ করতে পারবেন.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "O"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Igual a",
|
||||
"not_equal_to": "No és igual a",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Nom assignat",
|
||||
"INBOX_NAME": "Nom de la safata d'entrada",
|
||||
"TEAM_NAME": "Nom de l'equip",
|
||||
"CONTACT": "Contacte",
|
||||
"CONVERSATION_IDENTIFIER": "Identificador de conversa",
|
||||
"CAMPAIGN_NAME": "Nom de la campanya",
|
||||
"LABELS": "Etiquetes",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "No s'ha pogut obrir l'àudio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Arrossega i deixa anar aquí per adjuntar-lo",
|
||||
"IMAGE_UPLOAD_SUCCESS": "La imatge s'ha carregat correctament",
|
||||
"START_AUDIO_RECORDING": "Inicia la gravació d'àudio",
|
||||
"STOP_AUDIO_RECORDING": "Atura la gravació d'àudio",
|
||||
"COPILOT_THINKING": "Copilot està pensant",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Esborrar"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Nom"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Afegeix una nova localització",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Štítky",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Zvuk se nepodařilo otevřít",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Přetažením sem připojíte",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Spustit nahrávání zvuku",
|
||||
"STOP_AUDIO_RECORDING": "Zastavit nahrávání zvuku",
|
||||
"COPILOT_THINKING": "Copilot přemýšlí",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Vymazat"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Název"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "ELLER"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Lig med",
|
||||
"not_equal_to": "Ikke lig med",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Indbakkens navn",
|
||||
"TEAM_NAME": "Team navn",
|
||||
"CONTACT": "Kontakt",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Etiketter",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Kunne ikke åbne lyden",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Træk og slip her for at vedhæfte",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Billede blev uploadet",
|
||||
"START_AUDIO_RECORDING": "Start lydoptagelse",
|
||||
"STOP_AUDIO_RECORDING": "Stop lydoptagelse",
|
||||
"COPILOT_THINKING": "Copilot tænker",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Slet"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Navn"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Tilføj en ny landestandard",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "ODER"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Wert eintragen",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Kontakte suchen",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Gleich",
|
||||
"not_equal_to": "Nicht gleich",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Bearbeiter Name",
|
||||
"INBOX_NAME": "Posteingangsname",
|
||||
"TEAM_NAME": "Teamname",
|
||||
"CONTACT": "Kontakt",
|
||||
"CONVERSATION_IDENTIFIER": "Konversation-ID",
|
||||
"CAMPAIGN_NAME": "Kampagnenname",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Audio konnte nicht geöffnet werden",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Zum Anhängen hierher ziehen und ablegen",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Bild erfolgreich hochgeladen",
|
||||
"START_AUDIO_RECORDING": "Audioaufzeichnung starten",
|
||||
"STOP_AUDIO_RECORDING": "Audioaufzeichnung stoppen",
|
||||
"COPILOT_THINKING": "Copilot denkt",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Löschen"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Neues Gebietsschema hinzufügen",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Ίσο με",
|
||||
"not_equal_to": "Όχι ίσο με",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Όνομα Κιβωτίου",
|
||||
"TEAM_NAME": "Όνομα ομάδας",
|
||||
"CONTACT": "Επαφές",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Ετικέτες",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Αδυναμία ανοίγματος ήχου",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Σύρετε και αφήστε εδώ για επισύναψη",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Έναρξη ηχογράφησης",
|
||||
"STOP_AUDIO_RECORDING": "Διακοπή ηχογράφησης",
|
||||
"COPILOT_THINKING": "Ο Copilot σκέφτεται",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Διαγραφή"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Όνομα"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Προσθέστε μια νέα γλώσσα",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -51,7 +49,6 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,7 +233,6 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot is thinking",
|
||||
|
||||
@@ -700,27 +700,9 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Delete"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "O"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Introducir valor",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Buscar contactos",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Igual a",
|
||||
"not_equal_to": "No igual a",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Nombre Asignado",
|
||||
"INBOX_NAME": "Nombre de la bandeja de entrada",
|
||||
"TEAM_NAME": "Nombre del equipo",
|
||||
"CONTACT": "Contacto",
|
||||
"CONVERSATION_IDENTIFIER": "Identificador de conversacion",
|
||||
"CAMPAIGN_NAME": "Nombre de Campaña",
|
||||
"LABELS": "Etiquetas",
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"CALL": "Llamar",
|
||||
"CALL_INITIATED": "Llamando al contacto…",
|
||||
"CALL_FAILED": "No se puede iniciar la llamada. Por favor, inténtelo de nuevo.",
|
||||
"WHATSAPP_CALL_PERMISSION_REQUESTED": "Sent a call permission request to the contact. Try again once they accept.",
|
||||
"WHATSAPP_CALL_PERMISSION_PENDING": "Call permission request already sent recently. Try again once the contact accepts.",
|
||||
"WHATSAPP_CALL_PERMISSION_REQUESTED": "Solicitud de permiso de llamada al contacto enviada. Inténtalo de nuevo una vez que acepten.",
|
||||
"WHATSAPP_CALL_PERMISSION_PENDING": "Solicitud de permiso de llamada ya enviada recientemente. Inténtelo de nuevo una vez que el contacto acepte.",
|
||||
"CLICK_TO_EDIT": "Click to edit",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Seleccionar un buzón de entrada"
|
||||
@@ -585,11 +585,11 @@
|
||||
},
|
||||
"CONTACTS_BULK_ACTIONS": {
|
||||
"ASSIGN_LABELS": "Asignar etiquetas",
|
||||
"REMOVE_LABELS": "Remove Labels",
|
||||
"REMOVE_LABELS": "Quitar etiquetas",
|
||||
"ASSIGN_LABELS_SUCCESS": "Etiquetas asignadas correctamente.",
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"REMOVE_LABELS_SUCCESS": "Labels removed successfully.",
|
||||
"REMOVE_LABELS_FAILED": "Failed to remove labels",
|
||||
"REMOVE_LABELS_SUCCESS": "Etiquetas removidas con éxito.",
|
||||
"REMOVE_LABELS_FAILED": "Error al eliminar las etiquetas",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "Este mensaje no es compatible. Puedes ver este mensaje en la aplicación de Facebook Messenger.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "Este mensaje no es compatible. Puedes ver este mensaje en la aplicación de Instagram.",
|
||||
"UNSUPPORTED_MESSAGE_TIKTOK": "This message is unsupported. You can view this message on the TikTok app.",
|
||||
"UNSUPPORTED_MESSAGE_WHATSAPP": "This message is unsupported. You can view this message on the WhatsApp app.",
|
||||
"UNSUPPORTED_MESSAGE_WHATSAPP": "Este mensaje no es compatible. Puedes ver este mensaje en la aplicación de WhatsApp.",
|
||||
"SUCCESS_DELETE_MESSAGE": "Mensaje eliminado correctamente",
|
||||
"FAIL_DELETE_MESSSAGE": "¡No se pudo eliminar el mensaje! Inténtalo de nuevo",
|
||||
"NO_RESPONSE": "No hay respuesta",
|
||||
@@ -73,7 +73,7 @@
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "Mostrar etiquetas",
|
||||
"HIDE_LABELS": "Ocultar etiquetas",
|
||||
"LABELS_COUNT": "{count} labels"
|
||||
"LABELS_COUNT": "{count} etiquetas"
|
||||
},
|
||||
"VOICE_CALL": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
@@ -81,21 +81,21 @@
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NO_ANSWER": "No answer",
|
||||
"NO_ANSWER_OUTBOUND_LABEL": "No answer",
|
||||
"NO_ANSWER_OUTBOUND_SUBTEXT": "Contact didn't pick up",
|
||||
"NO_ANSWER_OUTBOUND_SUBTEXT": "El contacto no ha atendido",
|
||||
"MISSED_CALL": "Missed call",
|
||||
"MISSED_CALL_INBOUND_SUBTEXT": "No agent picked up",
|
||||
"MISSED_CALL_DECLINED_BY": "Declined by {agentName}",
|
||||
"MISSED_CALL_INBOUND_SUBTEXT": "Ningún agente escogido",
|
||||
"MISSED_CALL_DECLINED_BY": "Denegado por {agentName}",
|
||||
"CALL_ENDED": "Call ended",
|
||||
"HANDLED_BY": "Handled by {agentName}",
|
||||
"HANDLED_BY": "Manejado por {agentName}",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"CALLING": "Calling…",
|
||||
"CALLING": "Llamando…",
|
||||
"THEY_ANSWERED": "They answered",
|
||||
"YOU_ANSWERED": "You answered",
|
||||
"AGENT_ANSWERED": "{agentName} answered",
|
||||
"AGENT_ANSWERED": "{agentName} respondió",
|
||||
"JOIN_CALL": "Unirse a la llamada",
|
||||
"CALL_BACK": "Call back",
|
||||
"TRANSCRIPT_SHOW_MORE": "Show more",
|
||||
"TRANSCRIPT_SHOW_LESS": "Show less"
|
||||
"CALL_BACK": "Llamar de vuelta",
|
||||
"TRANSCRIPT_SHOW_MORE": "Mostrar más",
|
||||
"TRANSCRIPT_SHOW_LESS": "Mostrar menos"
|
||||
},
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolver",
|
||||
@@ -105,17 +105,17 @@
|
||||
"OPEN": "Más",
|
||||
"CLOSE": "Cerrar",
|
||||
"DETAILS": "detalles",
|
||||
"COPY_ID_SUCCESS": "Conversation ID copied to clipboard",
|
||||
"COPY_ID_SUCCESS": "ID de conversación copiado al portapapeles",
|
||||
"SNOOZED_UNTIL": "Posponer hasta",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Pospuesto hasta mañana",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Pospuesto hasta la próxima semana",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Posponer hasta la siguiente respuesta",
|
||||
"WHATSAPP_CALL": "Start WhatsApp call",
|
||||
"WHATSAPP_CALL_FAILED": "Could not start the WhatsApp call.",
|
||||
"VOICE_CALL": "Start call",
|
||||
"VOICE_CALL_FAILED": "Could not start the call.",
|
||||
"WHATSAPP_CALL_PERMISSION_REQUESTED": "Sent a call permission request to the contact. Try again once they accept.",
|
||||
"WHATSAPP_CALL_PERMISSION_PENDING": "Call permission request already sent recently. Try again once the contact accepts.",
|
||||
"WHATSAPP_CALL": "Iniciar llamada de WhatsApp",
|
||||
"WHATSAPP_CALL_FAILED": "No se pudo iniciar la llamada de WhatsApp.",
|
||||
"VOICE_CALL": "Iniciar llamada",
|
||||
"VOICE_CALL_FAILED": "No se pudo iniciar la llamada.",
|
||||
"WHATSAPP_CALL_PERMISSION_REQUESTED": "Solicitud de permiso de llamada al contacto enviada. Inténtalo de nuevo una vez que acepten.",
|
||||
"WHATSAPP_CALL_PERMISSION_PENDING": "Solicitud de permiso de llamada ya enviada recientemente. Inténtelo de nuevo una vez que el contacto acepte.",
|
||||
"SLA_STATUS": {
|
||||
"FRT": "FRT {status}",
|
||||
"NRT": "NRT {status}",
|
||||
@@ -231,8 +231,9 @@
|
||||
"TIP_AUDIORECORDER_ICON": "Grabar audio",
|
||||
"TIP_AUDIORECORDER_PERMISSION": "Permitir el acceso a audio",
|
||||
"TIP_AUDIORECORDER_ERROR": "No se pudo abrir el audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"AUDIO_CONVERSION_FAILED": "Error al convertir audio. Vuelve a intentarlo.",
|
||||
"DRAG_DROP": "Arrastra y suelta aquí para adjuntar",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Imagen subida con éxito",
|
||||
"START_AUDIO_RECORDING": "Iniciar grabación de audio",
|
||||
"STOP_AUDIO_RECORDING": "Detener grabación de audio",
|
||||
"COPILOT_THINKING": "Copilot está pensando",
|
||||
@@ -319,10 +320,10 @@
|
||||
"DISMISS_CALL": "Descartar",
|
||||
"JOIN_CALL": "Unirse a la llamada",
|
||||
"END_CALL": "Terminar llamada",
|
||||
"MUTE": "Mute mic",
|
||||
"UNMUTE": "Unmute mic",
|
||||
"VIEW_CHAT_HISTORY": "View chat history",
|
||||
"GO_TO_CONVERSATION": "Go to conversation thread"
|
||||
"MUTE": "Silenciar micrófono",
|
||||
"UNMUTE": "Reactivar micrófono",
|
||||
"VIEW_CHAT_HISTORY": "Ver historial de chat",
|
||||
"GO_TO_CONVERSATION": "Ir al hilo de conversación"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
@@ -389,18 +390,18 @@
|
||||
"MACROS": "Macros",
|
||||
"LINEAR_ISSUES": "Problemas lineales vinculados",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders",
|
||||
"SHARED_FILES": "Attachments"
|
||||
"SHARED_FILES": "Adjuntos"
|
||||
},
|
||||
"SHARED_FILES": {
|
||||
"EMPTY": "No attachments yet",
|
||||
"DOWNLOAD": "Download file",
|
||||
"DOWNLOAD_ERROR": "Could not download the file. Please try again.",
|
||||
"EMPTY": "No hay archivos adjuntos aún",
|
||||
"DOWNLOAD": "Descargar archivo",
|
||||
"DOWNLOAD_ERROR": "No se pudo descargar el archivo. Por favor, inténtelo de nuevo.",
|
||||
"MEDIA_HEADING": "Media",
|
||||
"FILES_HEADING": "Files",
|
||||
"FILES_HEADING": "Archivos",
|
||||
"VIEW_ALL": "Ver todo",
|
||||
"SHOW_LESS": "Show less",
|
||||
"SHOW_LESS": "Mostrar menos",
|
||||
"MORE_COUNT": "+{count}",
|
||||
"UNTITLED_FILE": "Untitled file"
|
||||
"UNTITLED_FILE": "Archivo sin título"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Eliminar"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Nombre"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Añadir un nuevo idioma",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||
"ENABLING_CALLING": "Enabling WhatsApp Calling on your number…",
|
||||
"ENABLING_CALLING": "Activando llamadas de WhatsApp en tu número…",
|
||||
"LOADING_SDK": "Loading Facebook SDK...",
|
||||
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||
@@ -319,7 +319,7 @@
|
||||
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured",
|
||||
"MANUAL_FALLBACK": "If your number is already connected to the WhatsApp Business Platform (API), or if you’re a tech provider onboarding your own number, please use the {link} flow",
|
||||
"MANUAL_LINK_TEXT": "manual setup flow",
|
||||
"CALLING_ENABLE_FAILED": "Your WhatsApp inbox is ready, but voice calling couldn't be turned on — this number isn't enrolled in the WhatsApp Business Calling API yet. Reach out to Meta or your WhatsApp Business Solution Provider to onboard it, then turn calling on from the inbox's Calls settings."
|
||||
"CALLING_ENABLE_FAILED": "Tu bandeja de entrada de WhatsApp está lista, pero las llamadas de voz no se pudieron activar — este número aún no está inscrito en la API de llamadas empresariales de WhatsApp. Llega a Meta o a tu proveedor de soluciones empresariales de WhatsApp a bordo, luego enciende la llamada desde la configuración de las llamadas de la bandeja de entrada."
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "No pudimos guardar el canal de WhatsApp"
|
||||
@@ -468,8 +468,8 @@
|
||||
"DESCRIPTION": "Support your customers on WhatsApp"
|
||||
},
|
||||
"WHATSAPP_CALL": {
|
||||
"TITLE": "WhatsApp Call",
|
||||
"DESCRIPTION": "Take voice calls on your WhatsApp number"
|
||||
"TITLE": "Llamada de WhatsApp",
|
||||
"DESCRIPTION": "Responde llamadas de voz en tu número de WhatsApp"
|
||||
},
|
||||
"EMAIL": {
|
||||
"TITLE": "E-mail",
|
||||
@@ -644,29 +644,29 @@
|
||||
"ACCOUNT_HEALTH": "Account Health",
|
||||
"CSAT": "Encuestas de Satisfacción",
|
||||
"VOICE": "Voice",
|
||||
"CALLS": "Calls"
|
||||
"CALLS": "Llamadas"
|
||||
},
|
||||
"VOICE_CONFIGURATION": {
|
||||
"ENABLE_VOICE": {
|
||||
"LABEL": "Enable Voice Calling",
|
||||
"DESCRIPTION": "Enable voice calling on this inbox. Agents will be able to make and receive phone calls."
|
||||
"LABEL": "Habilitar llamadas de voz",
|
||||
"DESCRIPTION": "Activar llamadas de voz en esta bandeja de entrada. Los agentes podrán hacer y recibir llamadas telefónicas."
|
||||
},
|
||||
"CREDENTIALS": {
|
||||
"DESCRIPTION": "Voice calling requires Twilio API Key credentials. These are used to generate tokens for agent voice connections."
|
||||
"DESCRIPTION": "Llamada de voz requiere credenciales de Clave de Twilio API. Estas se utilizan para generar tokens para las conexiones de voz del agente."
|
||||
}
|
||||
},
|
||||
"WHATSAPP_CALLING": {
|
||||
"ENABLE": {
|
||||
"LABEL": "Enable WhatsApp Calling",
|
||||
"DESCRIPTION": "Allow agents to receive and place WhatsApp Cloud calls on this inbox. Customers can call this business number directly from WhatsApp."
|
||||
"LABEL": "Habilitar llamadas de WhatsApp",
|
||||
"DESCRIPTION": "Permitir a los agentes recibir y hacer llamadas a WhatsApp Cloud en esta bandeja de entrada. Los clientes pueden llamar a este número de negocio directamente desde WhatsApp."
|
||||
},
|
||||
"ENABLE_FAILED": "Voice calling couldn't be turned on for this number — it isn't enrolled in the WhatsApp Business Calling API yet. Reach out to Meta or your WhatsApp Business Solution Provider to onboard it, then try again.",
|
||||
"ENABLE_FAILED": "No se pudo activar la llamada de voz para este número — aún no está inscrito en la API de llamadas empresariales de WhatsApp. Llega a Meta o a tu proveedor de soluciones de negocios de WhatsApp a bordo de él, luego vuelve a intentarlo.",
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Business phone number",
|
||||
"HELP_TEXT": "WhatsApp number that customers will call."
|
||||
"LABEL": "Número de teléfono empresarial",
|
||||
"HELP_TEXT": "Número de WhatsApp al que llamarán los clientes."
|
||||
},
|
||||
"HOW_IT_WORKS": {
|
||||
"LABEL": "How it works",
|
||||
"LABEL": "Cómo funciona",
|
||||
"DESCRIPTION": "Calls are placed peer-to-peer between the agent's browser and Meta — no extra credentials are required. Make sure the agent's browser has microphone permission for this site."
|
||||
},
|
||||
"PERMISSION_REQUEST_BODY": {
|
||||
|
||||
@@ -763,38 +763,38 @@
|
||||
},
|
||||
"FILTERS": {
|
||||
"SOURCE": {
|
||||
"ALL": "All sources",
|
||||
"WEB": "Web pages",
|
||||
"ALL": "Todas las fuentes",
|
||||
"WEB": "Páginas web",
|
||||
"PDF": "PDFs"
|
||||
},
|
||||
"STATUS": {
|
||||
"ANY": "Any status",
|
||||
"UPDATED": "Updated",
|
||||
"NEEDS_UPDATE": "Needs update",
|
||||
"UPDATING": "Updating",
|
||||
"ANY": "Cualquier estado",
|
||||
"UPDATED": "Actualizado",
|
||||
"NEEDS_UPDATE": "Necesita actualización",
|
||||
"UPDATING": "Actualizando",
|
||||
"FAILED": "Failed"
|
||||
},
|
||||
"SORT": {
|
||||
"RECENTLY_UPDATED": "Recently updated",
|
||||
"RECENTLY_CREATED": "Recently created"
|
||||
"RECENTLY_UPDATED": "Actualizado recientemente",
|
||||
"RECENTLY_CREATED": "Creado recientemente"
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Buscar..."
|
||||
},
|
||||
"SYNC_STATUS": {
|
||||
"SYNCED": "last updated {time}",
|
||||
"SYNCED": "última actualización {time}",
|
||||
"SYNCING": "actualizando...",
|
||||
"STALE_SYNC": "update stalled",
|
||||
"FAILED": "Failed to sync",
|
||||
"NEVER_SYNCED": "not updated yet"
|
||||
"FAILED": "Fallo al sincronizar",
|
||||
"NEVER_SYNCED": "no actualizado aún"
|
||||
},
|
||||
"SYNC_ERRORS": {
|
||||
"NOT_FOUND": "Página no encontrada",
|
||||
"ACCESS_DENIED": "Access denied",
|
||||
"TIMEOUT": "Page took too long to respond",
|
||||
"CONTENT_EMPTY": "Page returned empty content",
|
||||
"FETCH_FAILED": "Could not fetch page",
|
||||
"SYNC_ERROR": "Unexpected error",
|
||||
"DEFAULT": "Sync error"
|
||||
"ACCESS_DENIED": "Acceso denegado",
|
||||
"TIMEOUT": "La página tardó demasiado en responder",
|
||||
"CONTENT_EMPTY": "La página devolvió contenido vacío",
|
||||
"FETCH_FAILED": "No se pudo obtener la página",
|
||||
"SYNC_ERROR": "Error inesperado",
|
||||
"DEFAULT": "Error de sincronización"
|
||||
},
|
||||
"RELATED_RESPONSES": {
|
||||
"TITLE": "Related FAQs",
|
||||
@@ -839,14 +839,14 @@
|
||||
},
|
||||
"OPTIONS": {
|
||||
"VIEW_RELATED_RESPONSES": "View Related Responses",
|
||||
"SYNC_NOW": "Refresh now",
|
||||
"RETRY_SYNC": "Retry refresh",
|
||||
"SYNC_NOW": "Refrescar ahora",
|
||||
"RETRY_SYNC": "Reintentar refresco",
|
||||
"DELETE_DOCUMENT": "Delete Document"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No documents available",
|
||||
"SUBTITLE": "Documents are used by your assistant to generate FAQs. You can import documents to provide context for your assistant.",
|
||||
"FILTERED_TITLE": "No matching documents",
|
||||
"FILTERED_TITLE": "No hay documentos coincidentes",
|
||||
"FILTERED_SUBTITLE": "Try changing the source, status, or search term.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain Document",
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"SELECT_REFERRAL_SOURCE": "Select source"
|
||||
},
|
||||
"EMAIL_VERIFIED": "Email verified",
|
||||
"SETTING_UP": "Setting up your account...",
|
||||
"SETTING_UP": "Configurando tu cuenta...",
|
||||
"CONTINUE": "Continue",
|
||||
"SAVING": "Guardando...",
|
||||
"VALIDATION_ERROR": "Please fill in all required fields",
|
||||
"SUCCESS": "Details saved successfully",
|
||||
"ERROR": "Could not save details. Please try again."
|
||||
"VALIDATION_ERROR": "Por favor, rellene todos los campos requeridos",
|
||||
"SUCCESS": "Detalles guardados correctamente",
|
||||
"ERROR": "No se pudo guardar los detalles. Por favor, inténtelo de nuevo."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Otsi kontakte",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Kontakt",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Heli avamine ebaõnnestus",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Lohista siia manusena lisamiseks",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Pilt edukalt üles laaditud",
|
||||
"START_AUDIO_RECORDING": "Alusta heli salvestamist",
|
||||
"STOP_AUDIO_RECORDING": "Peata heli salvestamine",
|
||||
"COPILOT_THINKING": "Copilot mõtleb",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Määra vaikimisi",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Kustuta"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Lehe pealkiri"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Päise tekst"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Lisa uus keel",
|
||||
"DESCRIPTION": "Vali keel, milles see artikkel kirjutatakse. See lisatakse sinu tõlkelisti ja hiljem saad lisada veel.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "یا"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "جستجوی مخاطبین",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "مساوی با",
|
||||
"not_equal_to": "مساوی نیست با",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "نام مسئول",
|
||||
"INBOX_NAME": "نام صندوق ورودی",
|
||||
"TEAM_NAME": "نام تیم",
|
||||
"CONTACT": "مخاطب",
|
||||
"CONVERSATION_IDENTIFIER": "شناسه گفتگو",
|
||||
"CAMPAIGN_NAME": "نام کمپین",
|
||||
"LABELS": "برچسبها",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "صدا را نمیتوان باز کند",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "برای ضمیمه کردن درگ و درآپ کنید",
|
||||
"IMAGE_UPLOAD_SUCCESS": "تصویر با موفقیت آپلود شد",
|
||||
"START_AUDIO_RECORDING": "در حال شروع ضبط صدا",
|
||||
"STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا",
|
||||
"COPILOT_THINKING": "Copilot در حال فکر کردن است",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "حذف"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "نام"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "یک محل جدید اضافه کنید",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Tunnisteet",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot ajattelee",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Poista"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Nimi"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OU"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Égal à",
|
||||
"not_equal_to": "Pas égal à",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Nom du responsable",
|
||||
"INBOX_NAME": "Nom de la boîte de réception",
|
||||
"TEAM_NAME": "Nom de l'équipe",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Identifiant de la conversation",
|
||||
"CAMPAIGN_NAME": "Nom de la campagne",
|
||||
"LABELS": "Étiquettes",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Impossible d'ouvrir l'audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Glissez et déposez ici pour lier",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image téléchargée avec succès",
|
||||
"START_AUDIO_RECORDING": "Démarrer l'enregistrement audio",
|
||||
"STOP_AUDIO_RECORDING": "Arrêter l'enregistrement audio",
|
||||
"COPILOT_THINKING": "Copilot réfléchit",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Supprimer"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Nom"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Ajouter une nouvelle langue",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "או"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "הכנס ערך",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "חפש אנשי קשר",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "שווה ל",
|
||||
"not_equal_to": "לא שווה ל",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "שם מוקצה",
|
||||
"INBOX_NAME": "שם תיבת דואר נכנס",
|
||||
"TEAM_NAME": "שם צוות",
|
||||
"CONTACT": "איש קשר",
|
||||
"CONVERSATION_IDENTIFIER": "מזהה שיחה",
|
||||
"CAMPAIGN_NAME": "שם קמפיין",
|
||||
"LABELS": "תוויות",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "לא יכול לפתוח אודיו",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "גרור ושחרר כאן להוספת קובץ מצורף",
|
||||
"IMAGE_UPLOAD_SUCCESS": "התמונה הועלתה בהצלחה",
|
||||
"START_AUDIO_RECORDING": "התחל הקלטת אודיו",
|
||||
"STOP_AUDIO_RECORDING": "עצור הקלטת אודיו",
|
||||
"COPILOT_THINKING": "Copilot חושב",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "הפוך לברירת מחדל",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "מחק"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "שם"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "כותרת עמוד"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "טקסט כותרת עליונה"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "הוסף מקום חדש",
|
||||
"DESCRIPTION": "בחר את השפה שבה מאמר זה ייכתב. זו תתווסף לרשימת התרגומים שלך, ותוכל להוסיף נוספים מאוחר יותר.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"CONTACT_SEARCH_PLACEHOLDER": "Search contacts",
|
||||
"CONTACT_FALLBACK": "Contact #{id}",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
@@ -49,6 +51,7 @@
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONTACT": "Contact",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"AUDIO_CONVERSION_FAILED": "Audio conversion failed. Please try again.",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image uploaded successfully",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot सोच रहा है",
|
||||
|
||||
@@ -700,9 +700,27 @@
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"CUSTOMIZE_CONTENT": "Localize content",
|
||||
"DELETE": "Delete"
|
||||
}
|
||||
},
|
||||
"CONTENT_DIALOG": {
|
||||
"TITLE": "Localize content",
|
||||
"DESCRIPTION": "Set values specific to this locale. Anything left blank falls back to the default locale.",
|
||||
"NAME": {
|
||||
"LABEL": "Name"
|
||||
},
|
||||
"PAGE_TITLE": {
|
||||
"LABEL": "Page title"
|
||||
},
|
||||
"HEADER_TEXT": {
|
||||
"LABEL": "Header text"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale content updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update locale content. Try again."
|
||||
}
|
||||
},
|
||||
"ADD_LOCALE_DIALOG": {
|
||||
"TITLE": "Add a new locale",
|
||||
"DESCRIPTION": "Select the language in which this article will be written. This will be added to your list of translations, and you can add more later.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user