Merge branch 'release/2.11.0'

This commit is contained in:
Sojan 2022-11-16 00:45:34 +00:00
commit 8a0d6f6f50
740 changed files with 17226 additions and 2665 deletions

View file

@ -54,3 +54,5 @@ exclude_patterns:
- 'app/javascript/widget/i18n/index.js' - 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js' - 'app/javascript/survey/i18n/index.js'
- 'app/javascript/shared/constants/locales.js' - 'app/javascript/shared/constants/locales.js'
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'

View file

@ -34,6 +34,11 @@ REDIS_SENTINELS=
# You can find list of master using "SENTINEL masters" command # You can find list of master using "SENTINEL masters" command
REDIS_SENTINEL_MASTER_NAME= REDIS_SENTINEL_MASTER_NAME=
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
# Use the following environment variable to customize passwords for sentinels.
# Use empty string if sentinels are configured with out passwords
# REDIS_SENTINEL_PASSWORD=
# Redis premium breakage in heroku fix # Redis premium breakage in heroku fix
# enable the following configuration # enable the following configuration
# ref: https://github.com/chatwoot/chatwoot/issues/2420 # ref: https://github.com/chatwoot/chatwoot/issues/2420
@ -51,7 +56,7 @@ RAILS_MAX_THREADS=5
# The email from which all outgoing emails are sent # The email from which all outgoing emails are sent
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>` # could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>" MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
#SMTP domain key is set up for HELO checking #SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com SMTP_DOMAIN=chatwoot.com

View file

@ -6,6 +6,7 @@ labels: 'Bug'
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@ -16,11 +17,11 @@ Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See the error
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen. Share a clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
@ -28,27 +29,50 @@ If applicable, add screenshots to help explain your problem.
**Browser logs** **Browser logs**
Share the browser logs to debug the issue further Share the browser logs to debug the issue further.
**Server logs** **Server logs**
Share the server logs to debug the issue further Share the server logs to debug the issue further.
**Environment** **Environment**
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku) Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
**Desktop (please complete the following information):** - [ ] app.chatwoot.com (Chatwoot Cloud)
- OS: [e.g. iOS] - [ ] Self-hosted
- Browser [e.g. chrome, safari] - - [ ] Linux VM
- - [ ] Docker
- - [ ] Kubernetes
- - [ ] Heroku
- - [ ] Other (Please specify)
**Desktop (please complete the following information)** (If applicable)
- OS: [e.g. Linux, Windows, MacOS]
- Browser [e.g. chrome, firefox, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information)** (If applicable)
- Device: [e.g. iPhone6] - Device: [e.g. iPhone6, Pixel7]
- OS: [e.g. iOS8.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari] - Browser [e.g. stock browser, firefox, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Docker** (If applicable)
Please share the output of the following.
- `docker version`
- `docker info`
- `docker-compose version`
**Cloud Provider** (If applicable)
- [ ] AWS
- [ ] GCP
- [ ] Azure
- [ ] DigitalOcean
- [ ] Others
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -2,8 +2,7 @@
## Description ## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
Fixes # (issue) Fixes # (issue)
## Type of change ## Type of change
@ -12,18 +11,18 @@ Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
- [ ] This change requires a documentation update - [ ] This change requires a documentation update
## How Has This Been Tested? ## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
## Checklist: ## Checklist:
- [ ] My code follows the style guidelines of this project - [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code - [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have commented on my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation - [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings - [ ] My changes generate no new warnings

2
.gitignore vendored
View file

@ -60,3 +60,5 @@ test/cypress/videos/*
/config/master.key /config/master.key
/config/*.enc /config/*.enc
.vscode/settings.json

1
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -427,14 +427,14 @@ GEM
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (8.9.0) newrelic_rpm (8.9.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.7) nokogiri (1.13.9)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.7-arm64-darwin) nokogiri (1.13.9-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.7-x86_64-darwin) nokogiri (1.13.9-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.7-x86_64-linux) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.10) oauth (0.5.10)
orm_adapter (0.5.0) orm_adapter (0.5.0)
@ -808,4 +808,4 @@ RUBY VERSION
ruby 3.0.4p208 ruby 3.0.4p208
BUNDLED WITH BUNDLED WITH
2.3.18 2.3.16

View file

@ -72,6 +72,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
def build_message def build_message
return if @outgoing_echo && already_sent_from_chatwoot? return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@message = conversation.messages.create!(message_params) @message = conversation.messages.create!(message_params)
@ -117,6 +118,13 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
cw_message.present? cw_message.present?
end end
def all_unsupported_files?
return if attachments.empty?
attachments_type = attachments.pluck(:type).uniq.first
unsupported_file_type?(attachments_type)
end
### Sample response ### Sample response
# { # {
# "object": "instagram", # "object": "instagram",

View file

@ -35,7 +35,13 @@ class Messages::MessageBuilder
file: uploaded_attachment file: uploaded_attachment
) )
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile) attachment.file_type = if uploaded_attachment.is_a?(String)
file_type_by_signed_id(
uploaded_attachment
)
else
file_type(uploaded_attachment&.content_type)
end
end end
end end

View file

@ -2,7 +2,8 @@ class Messages::Messenger::MessageBuilder
include ::FileTypeHelper include ::FileTypeHelper
def process_attachment(attachment) def process_attachment(attachment)
return if attachment['type'].to_sym == :template # This check handles very rare case if there are multiple files to attach with only one usupported file
return if unsupported_file_type?(attachment['type'])
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save! attachment_obj.save!
@ -80,4 +81,10 @@ class Messages::Messenger::MessageBuilder
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{} {}
end end
private
def unsupported_file_type?(attachment_type)
[:template, :unsupported_type].include? attachment_type.to_sym
end
end end

View file

@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@articles_count = @portal.articles.count @portal_articles = @portal.articles
@articles = @portal.articles @all_articles = @portal_articles.search(list_params)
@articles = @articles.search(list_params) if list_params.present? @articles_count = @all_articles.count
@articles = @all_articles.page(@current_page)
end end
def create def create
@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
end end
def portal def portal
@portal ||= Current.account.portals.find_by(slug: params[:portal_id]) @portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
end end
def article_params def article_params

View file

@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@current_locale = params[:locale]
@categories = @portal.categories.search(params) @categories = @portal.categories.search(params)
end end

View file

@ -1,6 +1,6 @@
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_macro, only: [:show, :update, :destroy, :execute] before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
def index def index
@macros = Macro.with_visibility(current_user, params) @macros = Macro.with_visibility(current_user, params)
@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid? render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
@macro.save! @macro.save!
process_attachments
@macro
end end
def show def show
@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def update def update
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@macro.update!(macros_with_user) @macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params) @macro.set_visibility(current_user, permitted_params)
process_attachments
@macro.save! @macro.save!
rescue StandardError => e rescue StandardError => e
Rails.logger.error e Rails.logger.error e
@ -42,6 +55,19 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
private
def process_attachments
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
return if actions.blank?
actions.each do |action|
blob_id = action['action_params']
blob = ActiveStorage::Blob.find_by(id: blob_id)
@macro.files.attach(blob)
end
end
def permitted_params def permitted_params
params.permit( params.permit(
:name, :account_id, :visibility, :name, :account_id, :visibility,
@ -56,4 +82,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
def fetch_macro def fetch_macro
@macro = Current.account.macros.find_by(id: params[:id]) @macro = Current.account.macros.find_by(id: params[:id])
end end
def check_authorization
authorize(@macro) if @macro.present?
end
end end

View file

@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.members << agents @portal.members << agents
end end
def show; end def show
@all_articles = @portal.articles
@articles = @all_articles.search(locale: params[:locale])
end
def create def create
@portal = Current.account.portals.build(portal_params) @portal = Current.account.portals.build(portal_params)

View file

@ -50,7 +50,9 @@ class Api::V1::Widget::BaseController < ApplicationController
end end
def contact_name def contact_name
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present? return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present?
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
end end
def contact_phone_number def contact_phone_number

View file

@ -17,7 +17,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
@contact = ContactIdentifyAction.new( @contact = ContactIdentifyAction.new(
contact: @contact, contact: @contact,
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name }, params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
retain_original_contact_name: true retain_original_contact_name: true,
discard_invalid_attrs: true
).perform ).perform
end end

View file

@ -1,18 +1,17 @@
class Platform::Api::V1::AccountsController < PlatformController class Platform::Api::V1::AccountsController < PlatformController
def create def create
@resource = Account.new(account_params) @resource = Account.new(account_params)
update_resource_features
@resource.save! @resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end end
def show def show; end
render json: @resource
end
def update def update
@resource.update!(account_params) @resource.assign_attributes(account_params)
render json: @resource update_resource_features
@resource.save!
end end
def destroy def destroy
@ -27,6 +26,18 @@ class Platform::Api::V1::AccountsController < PlatformController
end end
def account_params def account_params
params.permit(:name, :locale) permitted_params.except(:features)
end
def update_resource_features
return if permitted_params[:features].blank?
permitted_params[:features].each do |key, value|
value.present? ? @resource.enable_features(key) : @resource.disable_features(key)
end
end
def permitted_params
params.permit(:name, :locale, :domain, :support_email, :status, features: {}, limits: {}, custom_attributes: {})
end end
end end

View file

@ -7,7 +7,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
@contact_inbox = ::ContactInboxWithContactBuilder.new( @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id, source_id: source_id,
inbox: @inbox_channel.inbox, inbox: @inbox_channel.inbox,
contact_attributes: permitted_params.except(:identifier, :identifier_hash) contact_attributes: permitted_params.except(:identifier_hash)
).perform ).perform
end end

View file

@ -3,9 +3,15 @@ class Public::Api::V1::InboxesController < PublicController
before_action :set_contact_inbox before_action :set_contact_inbox
before_action :set_conversation before_action :set_conversation
def show
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:id])
end
private private
def set_inbox_channel def set_inbox_channel
return if params[:inbox_id].blank?
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id]) @inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id])
end end

View file

@ -1,7 +1,7 @@
class Public::Api::V1::Portals::ArticlesController < PublicController class Public::Api::V1::Portals::ArticlesController < PublicController
before_action :ensure_custom_domain_request, only: [:show, :index] before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal before_action :portal
before_action :set_category before_action :set_category, except: [:index]
before_action :set_article, only: [:show] before_action :set_article, only: [:show]
layout 'portal' layout 'portal'
@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
end end
def set_category def set_category
@category = @portal.categories.find_by!(slug: params[:category_slug]) @category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
end end
def portal def portal

View file

@ -8,6 +8,12 @@ module FileTypeHelper
:file :file
end end
# Used in case of DIRECT_UPLOADS_ENABLED=true
def file_type_by_signed_id(signed_id)
blob = ActiveStorage::Blob.find_signed(signed_id)
file_type(blob&.content_type)
end
def image_file?(content_type) def image_file?(content_type)
[ [
'image/jpeg', 'image/jpeg',

View file

@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI {
super('categories', { accountScoped: true }); super('categories', { accountScoped: true });
} }
get({ portalSlug }) { get({ portalSlug, locale }) {
return axios.get(`${this.url}/${portalSlug}/categories`); return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
} }
create({ portalSlug, categoryObj }) { create({ portalSlug, categoryObj }) {

View file

@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient {
super('portals', { accountScoped: true }); super('portals', { accountScoped: true });
} }
getPortal({ portalSlug, locale }) {
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
}
updatePortal({ portalSlug, portalObj }) { updatePortal({ portalSlug, portalObj }) {
return axios.patch(`${this.url}/${portalSlug}`, portalObj); return axios.patch(`${this.url}/${portalSlug}`, portalObj);
} }

View file

@ -105,6 +105,16 @@ class ConversationApi extends ApiClient {
custom_attributes: customAttributes, custom_attributes: customAttributes,
}); });
} }
fetchParticipants(conversationId) {
return axios.get(`${this.url}/${conversationId}/participants`);
}
updateParticipants({ conversationId, userIds }) {
return axios.patch(`${this.url}/${conversationId}/participants`, {
user_ids: userIds,
});
}
} }
export default new ConversationApi(); export default new ConversationApi();

View file

@ -5,9 +5,12 @@ import Button from './ui/WootButton';
import Code from './Code'; import Code from './Code';
import ColorPicker from './widgets/ColorPicker'; import ColorPicker from './widgets/ColorPicker';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import ContextMenu from './ui/ContextMenu.vue';
import DeleteModal from './widgets/modal/DeleteModal.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import FeatureToggle from './widgets/FeatureToggle';
import HorizontalBar from './widgets/chart/HorizontalBarChart'; import HorizontalBar from './widgets/chart/HorizontalBarChart';
import Input from './widgets/forms/Input.vue'; import Input from './widgets/forms/Input.vue';
import Label from './ui/Label'; import Label from './ui/Label';
@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs'; import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem'; import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue'; import Thumbnail from './widgets/Thumbnail.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import ContextMenu from './ui/ContextMenu.vue';
const WootUIKit = { const WootUIKit = {
AvatarUploader, AvatarUploader,
@ -31,9 +32,12 @@ const WootUIKit = {
Code, Code,
ColorPicker, ColorPicker,
ConfirmDeleteModal, ConfirmDeleteModal,
ConfirmModal,
ContextMenu,
DeleteModal, DeleteModal,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
FeatureToggle,
HorizontalBar, HorizontalBar,
Input, Input,
Label, Label,
@ -47,8 +51,6 @@ const WootUIKit = {
Tabs, Tabs,
TabsItem, TabsItem,
Thumbnail, Thumbnail,
ConfirmModal,
ContextMenu,
install(Vue) { install(Vue) {
const keys = Object.keys(this); const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys keys.pop(); // remove 'install' from keys

View file

@ -26,7 +26,7 @@
:class="{ 'text-truncate': shouldTruncate }" :class="{ 'text-truncate': shouldTruncate }"
> >
{{ label }} {{ label }}
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view"> <span v-if="showChildCount" class="count-view">
{{ childItemCount }} {{ childItemCount }}
</span> </span>
</span> </span>
@ -76,7 +76,7 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isHelpCenterSidebar: { showChildCount: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab);
color: var(--w-500); color: var(--w-500);
border-color: var(--w-25); border-color: var(--w-25);
} }
&.is-active .count-view {
background: var(--w-75);
color: var(--w-500);
}
} }
.menu-label { .menu-label {
flex-grow: 1; flex-grow: 1;
line-height: var(--space-two); display: inline-flex;
align-items: center;
} }
.inbox-icon { .inbox-icon {
@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller); margin-left: var(--space-smaller);
padding: var(--space-zero) var(--space-smaller); padding: var(--space-zero) var(--space-smaller);
line-height: var(--font-size-small);
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
} }
</style> </style>

View file

@ -4,16 +4,15 @@
<span class="secondary-menu--header fs-small"> <span class="secondary-menu--header fs-small">
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
</span> </span>
<div v-if="isHelpCenterSidebar" class="submenu-icons"> <div v-if="menuItem.showNewButton" class="submenu-icons">
<woot-button <woot-button
size="tiny" size="tiny"
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
icon="add"
class="submenu-icon" class="submenu-icon"
@click="onClickOpen" @click="onClickOpen"
> />
<fluent-icon icon="add" size="16" />
</woot-button>
</div> </div>
</div> </div>
<router-link <router-link
@ -28,11 +27,7 @@
size="14" size="14"
/> />
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
<span <span v-if="showChildCount(menuItem.count)" class="count-view">
v-if="isHelpCenterSidebar"
class="count-view"
:class="computedClass"
>
{{ `${menuItem.count}` }} {{ `${menuItem.count}` }}
</span> </span>
<span <span
@ -55,7 +50,7 @@
:should-truncate="child.truncateLabel" :should-truncate="child.truncateLabel"
:icon="computedInboxClass(child)" :icon="computedInboxClass(child)"
:warning-icon="computedInboxErrorClass(child)" :warning-icon="computedInboxErrorClass(child)"
:is-help-center-sidebar="isHelpCenterSidebar" :show-child-count="showChildCount(child.count)"
:child-item-count="child.count" :child-item-count="child.count"
/> />
<router-link <router-link
@ -64,10 +59,10 @@
:to="menuItem.toState" :to="menuItem.toState"
custom custom
> >
<li> <li class="menu-item--new">
<a <a
:href="href" :href="href"
class="button small clear menu-item--new secondary" class="button small link clear secondary"
:class="{ 'is-active': isActive }" :class="{ 'is-active': isActive }"
@click="e => newLinkClick(e, navigate)" @click="e => newLinkClick(e, navigate)"
> >
@ -78,9 +73,6 @@
</a> </a>
</li> </li>
</router-link> </router-link>
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
</p>
</ul> </ul>
</li> </li>
</template> </template>
@ -104,14 +96,6 @@ export default {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
isHelpCenterSidebar: {
type: Boolean,
default: false,
},
isCategoryEmpty: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -161,8 +145,8 @@ export default {
this.menuItem.toStateName === 'settings_applications' this.menuItem.toStateName === 'settings_applications'
); );
}, },
isArticlesView() { isCurrentRoute() {
return this.$store.state.route.name === this.menuItem.toStateName; return this.$store.state.route.name.includes(this.menuItem.toStateName);
}, },
computedClass() { computedClass() {
@ -181,12 +165,11 @@ export default {
} }
return ' '; return ' ';
} }
if (this.isHelpCenterSidebar) {
if (this.isArticlesView) { if (this.isCurrentRoute) {
return 'is-active'; return 'is-active';
}
return ' ';
} }
return ''; return '';
}, },
}, },
@ -222,6 +205,9 @@ export default {
onClickOpen() { onClickOpen() {
this.$emit('open'); this.$emit('open');
}, },
showChildCount(count) {
return Number.isInteger(count);
},
}, },
}; };
</script> </script>
@ -277,6 +263,11 @@ export default {
color: var(--w-500); color: var(--w-500);
border-color: var(--w-25); border-color: var(--w-25);
} }
&.is-active .count-view {
background: var(--w-75);
color: var(--w-600);
}
} }
.secondary-menu--icon { .secondary-menu--icon {
@ -306,15 +297,12 @@ export default {
top: -1px; top: -1px;
} }
.sidebar-item .button.menu-item--new { .sidebar-item .menu-item--new {
display: inline-flex; padding: var(--space-small) 0;
height: var(--space-medium);
margin: var(--space-smaller) 0;
padding: var(--space-smaller);
color: var(--s-500);
&:hover { .button {
color: var(--w-500); display: inline-flex;
color: var(--s-500);
} }
} }
@ -340,11 +328,6 @@ export default {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller); margin-left: var(--space-smaller);
padding: var(--space-zero) var(--space-smaller); padding: var(--space-zero) var(--space-smaller);
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
} }
.submenu-icons { .submenu-icons {
@ -356,10 +339,4 @@ export default {
margin-left: var(--space-small); margin-left: var(--space-small);
} }
} }
.empty-text {
color: var(--s-500);
font-size: var(--font-size-small);
margin: var(--space-smaller);
}
</style> </style>

View file

@ -1,17 +1,15 @@
<template> <template>
<span class="time-ago"> <span class="time-ago">
<span> {{ timeAgo }}</span> <span>{{ timeAgo }}</span>
</span> </span>
</template> </template>
<script> <script>
const ZERO = 0;
const MINUTE_IN_MILLI_SECONDS = 60000; const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60; const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24; const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
import timeMixin from 'dashboard/mixins/time'; import timeMixin from 'dashboard/mixins/time';
import { differenceInMilliseconds } from 'date-fns';
export default { export default {
name: 'TimeAgo', name: 'TimeAgo',
@ -28,51 +26,40 @@ export default {
}, },
data() { data() {
return { return {
timeAgo: '', timeAgo: this.dynamicTime(this.timestamp),
timer: null, timer: null,
}; };
}, },
computed: { watch: {
timestamp() {
this.timeAgo = this.dynamicTime(this.timestamp);
},
},
mounted() {
if (this.isAutoRefreshEnabled) {
this.createTimer();
}
},
beforeDestroy() {
clearTimeout(this.timer);
},
methods: {
createTimer() {
this.timer = setTimeout(() => {
this.timeAgo = this.dynamicTime(this.timestamp);
this.createTimer();
}, this.refreshTime());
},
refreshTime() { refreshTime() {
const timeDiff = differenceInMilliseconds( const timeDiff = Date.now() - this.timestamp * 1000;
new Date(),
new Date(this.timestamp * 1000)
);
if (timeDiff > DAY_IN_MILLI_SECONDS) { if (timeDiff > DAY_IN_MILLI_SECONDS) {
return DAY_IN_MILLI_SECONDS; return DAY_IN_MILLI_SECONDS;
} }
if (timeDiff > HOUR_IN_MILLI_SECONDS) { if (timeDiff > HOUR_IN_MILLI_SECONDS) {
return HOUR_IN_MILLI_SECONDS; return HOUR_IN_MILLI_SECONDS;
} }
if (timeDiff > MINUTE_IN_MILLI_SECONDS) {
return MINUTE_IN_MILLI_SECONDS; return MINUTE_IN_MILLI_SECONDS;
}
return ZERO;
},
},
mounted() {
this.timeAgo = this.dynamicTime(this.timestamp);
if (this.isAutoRefreshEnabled) {
this.createTimer();
}
},
beforeDestroy() {
this.clearTimer();
},
methods: {
createTimer() {
const refreshTime = this.refreshTime;
if (refreshTime > ZERO) {
this.timer = setTimeout(() => {
this.timeAgo = this.dynamicTime(this.timestamp);
this.createTimer();
}, refreshTime);
}
},
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
}
}, },
}, },
}; };

View file

@ -1,8 +1,5 @@
<template> <template>
<div <div class="filter" :class="actionInputStyles">
class="filter"
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
>
<div class="filter-inputs"> <div class="filter-inputs">
<select <select
v-model="action_name" v-model="action_name"
@ -21,14 +18,32 @@
<div v-if="showActionInput" class="filter__answer--wrap"> <div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType"> <div v-if="inputType">
<div <div
v-if="inputType === 'multi_select'" v-if="inputType === 'search_select'"
class="multiselect-wrap--small" class="multiselect-wrap--small"
> >
<multiselect <multiselect
v-model="action_params" v-model="action_params"
track-by="id" track-by="id"
label="name" label="name"
:placeholder="'Select'" :placeholder="$t('FORMS.MULTISELECT.SELECT')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div
v-else-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
:multiple="true" :multiple="true"
selected-label selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')" :select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
@ -36,6 +51,7 @@
:max-height="160" :max-height="160"
:options="dropdownValues" :options="dropdownValues"
:allow-empty="false" :allow-empty="false"
:option-height="104"
/> />
</div> </div>
<input <input
@ -60,6 +76,7 @@
</div> </div>
</div> </div>
<woot-button <woot-button
v-if="!isMacro"
icon="dismiss" icon="dismiss"
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
@ -120,6 +137,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isMacro: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
action_name: { action_name: {
@ -146,6 +167,12 @@ export default {
return this.actionTypes.find(action => action.key === this.action_name) return this.actionTypes.find(action => action.key === this.action_name)
.inputType; .inputType;
}, },
actionInputStyles() {
return {
'has-error': this.v.action_params.$dirty && this.v.action_params.$error,
'is-a-macro': this.isMacro,
};
},
}, },
methods: { methods: {
removeAction() { removeAction() {
@ -165,9 +192,21 @@ export default {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius-medium); border-radius: var(--border-radius-medium);
margin-bottom: var(--space-small); margin-bottom: var(--space-small);
&.is-a-macro {
margin-bottom: 0;
background: var(--white);
padding: var(--space-zero);
border: unset;
border-radius: unset;
}
} }
.filter.error { .no-margin-bottom {
margin-bottom: 0;
}
.filter.has-error {
background: var(--r-50); background: var(--r-50);
} }
@ -240,6 +279,6 @@ export default {
margin-bottom: var(--space-zero); margin-bottom: var(--space-zero);
} }
.action-message { .action-message {
margin: var(--space-small) 0 0; margin: var(--space-small) var(--space-zero) var(--space-zero);
} }
</style> </style>

View file

@ -113,5 +113,6 @@ input[type='file'] {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
margin-bottom: 0;
} }
</style> </style>

View file

@ -1,10 +1,6 @@
<template> <template>
<div <div class="avatar-container" :style="style" aria-hidden="true">
class="avatar-container" {{ userInitial }}
:style="[style, customStyle]"
aria-hidden="true"
>
<span>{{ userInitial }}</span>
</div> </div>
</template> </template>
@ -16,69 +12,26 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
backgroundColor: {
type: String,
default: '#c2e1ff',
},
color: {
type: String,
default: '#1976cc',
},
customStyle: {
type: Object,
default: undefined,
},
size: { size: {
type: Number, type: Number,
default: 40, default: 40,
}, },
src: {
type: String,
default: '',
},
rounded: {
type: Boolean,
default: true,
},
variant: {
type: String,
default: 'circle',
},
}, },
computed: { computed: {
style() { style() {
let style = { return {
width: `${this.size}px`,
height: `${this.size}px`,
borderRadius:
this.variant === 'square' ? 'var(--border-radius-large)' : '50%',
lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
fontSize: `${Math.floor(this.size / 2.5)}px`, fontSize: `${Math.floor(this.size / 2.5)}px`,
}; };
if (this.backgroundColor) {
style = { ...style, backgroundColor: this.backgroundColor };
}
if (this.color) {
style = { ...style, color: this.color };
}
return style;
}, },
userInitial() { userInitial() {
return this.initials || this.initial(this.username); const parts = this.username.split(/[ -]/);
}, let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
},
methods: {
initial(username) {
const parts = username ? username.split(/[ -]/) : [];
let initials = '';
for (let i = 0; i < parts.length; i += 1) {
initials += parts[i].charAt(0);
}
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) { if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
initials = initials.replace(/[a-z]+/g, ''); initials = initials.replace(/[a-z]+/g, '');
} }
initials = initials.substring(0, 2).toUpperCase(); initials = initials.substring(0, 2).toUpperCase();
return initials; return initials;
}, },
}, },
@ -88,11 +41,13 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.avatar-container { .avatar-container {
display: flex; display: flex;
line-height: 100%;
font-weight: 500; font-weight: 500;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%); background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
color: var(--w-600); color: var(--w-600);
cursor: default;
} }
</style> </style>

View file

@ -5,6 +5,11 @@
:key="index" :key="index"
class="dashboard-app--list" class="dashboard-app--list"
> >
<loading-state
v-if="iframeLoading"
:message="$t('DASHBOARD_APPS.LOADING_MESSAGE')"
class="dashboard-app_loading-container"
/>
<iframe <iframe
v-if="configItem.type === 'frame' && configItem.url" v-if="configItem.type === 'frame' && configItem.url"
:id="`dashboard-app--frame-${index}`" :id="`dashboard-app--frame-${index}`"
@ -16,7 +21,11 @@
</template> </template>
<script> <script>
import LoadingState from 'dashboard/components/widgets/LoadingState';
export default { export default {
components: {
LoadingState,
},
props: { props: {
config: { config: {
type: Array, type: Array,
@ -27,6 +36,11 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
data() {
return {
iframeLoading: true,
};
},
computed: { computed: {
dashboardAppContext() { dashboardAppContext() {
return { return {
@ -57,6 +71,7 @@ export default {
); );
const eventData = { event: 'appContext', data: this.dashboardAppContext }; const eventData = { event: 'appContext', data: this.dashboardAppContext };
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*'); frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
this.iframeLoading = false;
}, },
}, },
}; };
@ -73,4 +88,11 @@ export default {
.dashboard-app--list iframe { .dashboard-app--list iframe {
border: 0; border: 0;
} }
.dashboard-app_loading-container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
</style> </style>

View file

@ -0,0 +1,25 @@
<template>
<div v-if="isFeatureEnabled">
<slot />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
props: {
featureKey: {
type: String,
required: true,
},
},
computed: {
...mapGetters({
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
accountId: 'getCurrentAccountId',
}),
isFeatureEnabled() {
return this.isFeatureEnabledonAccount(this.accountId, this.featureKey);
},
},
};
</script>

View file

@ -32,6 +32,7 @@
v-for="attribute in filterAttributes" v-for="attribute in filterAttributes"
:key="attribute.key" :key="attribute.key"
:value="attribute.key" :value="attribute.key"
:disabled="attribute.disabled"
> >
{{ attribute.name }} {{ attribute.name }}
</option> </option>
@ -173,6 +174,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
customAttributeType: {
type: String,
default: '',
},
}, },
computed: { computed: {
attributeKey: { attributeKey: {

View file

@ -83,75 +83,71 @@ export default {
}, },
pageSize: { pageSize: {
type: Number, type: Number,
default: 15, default: 25,
}, },
totalCount: { totalCount: {
type: Number, type: Number,
default: 0, default: 0,
}, },
onPageChange: {
type: Function,
default: () => {},
},
}, },
computed: { computed: {
isFooterVisible() { isFooterVisible() {
return this.totalCount && !(this.firstIndex > this.totalCount); return this.totalCount && !(this.firstIndex > this.totalCount);
}, },
firstIndex() { firstIndex() {
const firstIndex = this.pageSize * (this.currentPage - 1) + 1; return this.pageSize * (this.currentPage - 1) + 1;
return firstIndex;
}, },
lastIndex() { lastIndex() {
const index = Math.min(this.totalCount, this.pageSize * this.currentPage); return Math.min(this.totalCount, this.pageSize * this.currentPage);
return index;
}, },
searchButtonClass() { searchButtonClass() {
return this.searchQuery !== '' ? 'show' : ''; return this.searchQuery !== '' ? 'show' : '';
}, },
hasLastPage() { hasLastPage() {
const isDisabled = return !!Math.ceil(this.totalCount / this.pageSize);
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
return isDisabled;
}, },
hasFirstPage() { hasFirstPage() {
const isDisabled = this.currentPage === 1; return this.currentPage === 1;
return isDisabled;
}, },
hasNextPage() { hasNextPage() {
const isDisabled = return this.currentPage === Math.ceil(this.totalCount / this.pageSize);
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
return isDisabled;
}, },
hasPrevPage() { hasPrevPage() {
const isDisabled = this.currentPage === 1; return this.currentPage === 1;
return isDisabled;
}, },
}, },
methods: { methods: {
onNextPage() { onNextPage() {
if (this.hasNextPage) return; if (this.hasNextPage) {
return;
}
const newPage = this.currentPage + 1; const newPage = this.currentPage + 1;
this.onPageChange(newPage); this.onPageChange(newPage);
}, },
onPrevPage() { onPrevPage() {
if (this.hasPrevPage) return; if (this.hasPrevPage) {
return;
}
const newPage = this.currentPage - 1; const newPage = this.currentPage - 1;
this.onPageChange(newPage); this.onPageChange(newPage);
}, },
onFirstPage() { onFirstPage() {
if (this.hasFirstPage) return; if (this.hasFirstPage) {
return;
}
const newPage = 1; const newPage = 1;
this.onPageChange(newPage); this.onPageChange(newPage);
}, },
onLastPage() { onLastPage() {
if (this.hasLastPage) return; if (this.hasLastPage) {
return;
}
const newPage = Math.ceil(this.totalCount / this.pageSize); const newPage = Math.ceil(this.totalCount / this.pageSize);
this.onPageChange(newPage); this.onPageChange(newPage);
}, },
onPageChange(page) {
this.$emit('page-change', page);
},
}, },
}; };
</script> </script>

View file

@ -2,49 +2,47 @@ import { mount } from '@vue/test-utils';
import Avatar from './Avatar.vue'; import Avatar from './Avatar.vue';
import Thumbnail from './Thumbnail.vue'; import Thumbnail from './Thumbnail.vue';
describe(`when there are NO errors loading the thumbnail`, () => { describe('Thumbnail.vue', () => {
it(`should render the agent thumbnail`, () => { it('should render the agent thumbnail if valid image is passed', () => {
const wrapper = mount(Thumbnail, { const wrapper = mount(Thumbnail, {
propsData: { propsData: {
src: 'https://some_valid_url.com', src: 'https://some_valid_url.com',
}, },
data() { data() {
return { return {
hasImageLoaded: true,
imgError: false, imgError: false,
}; };
}, },
}); });
expect(wrapper.find('#image').exists()).toBe(true); expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
const avatarComponent = wrapper.findComponent(Avatar); const avatarComponent = wrapper.findComponent(Avatar);
expect(avatarComponent.exists()).toBe(false); expect(avatarComponent.isVisible()).toBe(false);
}); });
});
describe(`when there ARE errors loading the thumbnail`, () => { it('should render the avatar component if invalid image is passed', () => {
it(`should render the agent avatar`, () => {
const wrapper = mount(Thumbnail, { const wrapper = mount(Thumbnail, {
propsData: { propsData: {
src: 'https://some_invalid_url.com', src: 'https://some_invalid_url.com',
}, },
data() { data() {
return { return {
hasImageLoaded: true,
imgError: true, imgError: true,
}; };
}, },
}); });
expect(wrapper.find('#image').exists()).toBe(false); expect(wrapper.find('#image').exists()).toBe(false);
const avatarComponent = wrapper.findComponent(Avatar); const avatarComponent = wrapper.findComponent(Avatar);
expect(avatarComponent.exists()).toBe(true); expect(avatarComponent.isVisible()).toBe(true);
}); });
});
describe(`when Avatar shows`, () => { it('should the initial of the name if no image is passed', () => {
it(`initials shold correspond to username`, () => {
const wrapper = mount(Avatar, { const wrapper = mount(Avatar, {
propsData: { propsData: {
username: 'Angie Rojas', username: 'Angie Rojas',
}, },
}); });
expect(wrapper.find('span').text()).toBe('AR'); expect(wrapper.find('div').text()).toBe('AR');
}); });
}); });

View file

@ -1,74 +1,29 @@
<template> <template>
<div class="user-thumbnail-box" :style="{ height: size, width: size }"> <div
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<img <img
v-if="!imgError && Boolean(src)" v-show="shouldShowImage"
id="image"
:src="src" :src="src"
:class="thumbnailClass" :class="thumbnailClass"
@error="onImgError()" @load="onImgLoad"
@error="onImgError"
/> />
<Avatar <Avatar
v-else v-show="!shouldShowImage"
:username="userNameWithoutEmoji" :username="userNameWithoutEmoji"
:class="thumbnailClass" :class="thumbnailClass"
:size="avatarSize" :size="avatarSize"
:variant="variant"
/> />
<img <img
v-if="badge === 'instagram_direct_message'" v-if="badgeSrc"
id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="/integrations/channels/badges/instagram-dm.png" :src="`/integrations/channels/badges/${badgeSrc}.png`"
/> alt="Badge"
<img
v-else-if="badge === 'facebook'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/messenger.png"
/>
<img
v-else-if="badge === 'twitter-tweet'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/twitter-tweet.png"
/>
<img
v-else-if="badge === 'twitter-dm'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/twitter-dm.png"
/>
<img
v-else-if="badge === 'whatsapp'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/whatsapp.png"
/>
<img
v-else-if="badge === 'sms'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/sms.png"
/>
<img
v-else-if="badge === 'Channel::Line'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/line.png"
/>
<img
v-else-if="badge === 'Channel::Telegram'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/telegram.png"
/> />
<div <div
v-if="showStatusIndicator" v-if="showStatusIndicator"
@ -83,7 +38,7 @@
* Src - source for round image * Src - source for round image
* Size - Size of the thumbnail * Size - Size of the thumbnail
* Badge - Chat source indication { fb / telegram } * Badge - Chat source indication { fb / telegram }
* Username - User name for avatar * Username - Username for avatar
*/ */
import Avatar from './Avatar'; import Avatar from './Avatar';
import { removeEmoji } from 'shared/helpers/emoji'; import { removeEmoji } from 'shared/helpers/emoji';
@ -103,7 +58,7 @@ export default {
}, },
badge: { badge: {
type: String, type: String,
default: 'fb', default: '',
}, },
username: { username: {
type: String, type: String,
@ -121,6 +76,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
title: {
type: String,
default: '',
},
variant: { variant: {
type: String, type: String,
default: 'circle', default: 'circle',
@ -128,6 +87,7 @@ export default {
}, },
data() { data() {
return { return {
hasImageLoaded: false,
imgError: false, imgError: false,
}; };
}, },
@ -142,6 +102,19 @@ export default {
avatarSize() { avatarSize() {
return Number(this.size.replace(/\D+/g, '')); return Number(this.size.replace(/\D+/g, ''));
}, },
badgeSrc() {
return {
instagram_direct_message: 'instagram-dm',
facebook: 'messenger',
'twitter-tweet': 'twitter-tweet',
'twitter-dm': 'twitter-dm',
whatsapp: 'whatsapp',
sms: 'sms',
'Channel::Line': 'line',
'Channel::Telegram': 'telegram',
'Channel::WebWidget': '',
}[this.badge];
},
badgeStyle() { badgeStyle() {
const size = Math.floor(this.avatarSize / 3); const size = Math.floor(this.avatarSize / 3);
const badgeSize = `${size + 2}px`; const badgeSize = `${size + 2}px`;
@ -158,20 +131,34 @@ export default {
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square'; this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
return `user-thumbnail ${classname} ${variant}`; return `user-thumbnail ${classname} ${variant}`;
}, },
thumbnailBoxClass() {
const boxClass = this.variant === 'circle' ? 'is-rounded' : '';
return `user-thumbnail-box ${boxClass}`;
},
shouldShowImage() {
if (!this.src) {
return false;
}
if (this.hasImageLoaded) {
return !this.imgError;
}
return false;
},
}, },
watch: { watch: {
src: { src(value, oldValue) {
handler(value, oldValue) { if (value !== oldValue && this.imgError) {
if (value !== oldValue && this.imgError) { this.imgError = false;
this.imgError = false; }
}
},
}, },
}, },
methods: { methods: {
onImgError() { onImgError() {
this.imgError = true; this.imgError = true;
}, },
onImgLoad() {
this.hasImageLoaded = true;
},
}, },
}; };
</script> </script>
@ -182,6 +169,10 @@ export default {
max-width: 100%; max-width: 100%;
position: relative; position: relative;
&.is-rounded {
border-radius: 50%;
}
.user-thumbnail { .user-thumbnail {
border-radius: 50%; border-radius: 50%;
&.thumbnail-square { &.thumbnail-square {
@ -191,6 +182,7 @@ export default {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
object-fit: cover; object-fit: cover;
vertical-align: initial;
&.border { &.border {
border: 1px solid white; border: 1px solid white;
@ -229,9 +221,5 @@ export default {
.user-online-status--offline { .user-online-status--offline {
background: var(--s-500); background: var(--s-500);
} }
.user-online-status--offline {
background: var(--s-500);
}
} }
</style> </style>

View file

@ -0,0 +1,89 @@
<template>
<div class="overlapping-thumbnails">
<thumbnail
v-for="user in usersList"
:key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail"
:username="user.name"
:has-border="true"
:size="size"
:class="`overlapping-thumbnail gap-${gap}`"
/>
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
{{ moreThumbnailsText }}
</span>
</div>
</template>
<script>
import Thumbnail from './Thumbnail';
export default {
components: {
Thumbnail,
},
props: {
usersList: {
type: Array,
default: () => [],
},
size: {
type: String,
default: '24px',
},
showMoreThumbnailsCount: {
type: Boolean,
default: false,
},
moreThumbnailsText: {
type: String,
default: '',
},
gap: {
type: String,
default: 'normal',
validator(value) {
// The value must match one of these strings
return ['normal', '', 'tight'].includes(value);
},
},
},
};
</script>
<style lang="scss" scoped>
.overlapping-thumbnails {
display: flex;
}
.overlapping-thumbnail {
position: relative;
box-shadow: var(--shadow-small);
&:not(:first-child) {
margin-left: var(--space-minus-smaller);
}
.gap-tight {
margin-left: var(--space-minus-small);
}
}
.thumbnail-more-text {
display: inline-flex;
align-items: center;
position: relative;
margin-left: var(--space-minus-small);
padding: 0 var(--space-small);
box-shadow: var(--shadow-small);
background: var(--color-background);
border-radius: var(--space-giga);
border: 1px solid var(--white);
color: var(--s-600);
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
}
</style>

View file

@ -42,7 +42,7 @@
</div> </div>
<dashboard-app-frame <dashboard-app-frame
v-else v-else
:key="currentChat.id" :key="currentChat.id + '-' + activeIndex"
:config="dashboardApps[activeIndex - 1].content" :config="dashboardApps[activeIndex - 1].content"
:current-chat="currentChat" :current-chat="currentChat"
/> />

View file

@ -40,6 +40,12 @@
:url="attachment.data_url" :url="attachment.data_url"
:readable-time="readableTime" :readable-time="readableTime"
/> />
<bubble-location
v-else-if="attachment.file_type === 'location'"
:latitude="attachment.coordinates_lat"
:longitude="attachment.coordinates_long"
:name="attachment.fallback_title"
/>
<bubble-file <bubble-file
v-else v-else
:url="attachment.data_url" :url="attachment.data_url"
@ -119,6 +125,7 @@ import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File'; import BubbleFile from './bubble/File';
import BubbleVideo from './bubble/Video.vue'; import BubbleVideo from './bubble/Video.vue';
import BubbleActions from './bubble/Actions'; import BubbleActions from './bubble/Actions';
import BubbleLocation from './bubble/Location';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
@ -136,6 +143,7 @@ export default {
BubbleFile, BubbleFile,
BubbleVideo, BubbleVideo,
BubbleMailHead, BubbleMailHead,
BubbleLocation,
ContextMenu, ContextMenu,
Spinner, Spinner,
}, },

View file

@ -139,7 +139,6 @@ export default {
listLoadingStatus: 'getAllMessagesLoaded', listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount', getUnreadCount: 'getUnreadCount',
loadingChatList: 'getChatListLoadingStatus', loadingChatList: 'getChatListLoadingStatus',
conversationLastSeen: 'getConversationLastSeen',
}), }),
inboxId() { inboxId() {
return this.currentChat.inbox_id; return this.currentChat.inbox_id;
@ -234,7 +233,6 @@ export default {
return 'arrow-chevron-left'; return 'arrow-chevron-left';
}, },
getLastSeenAt() { getLastSeenAt() {
if (this.conversationLastSeen) return this.conversationLastSeen;
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat; const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt; return contactLastSeenAt;
}, },

View file

@ -0,0 +1,78 @@
<template>
<div class="location message-text__wrap">
<div class="icon-wrap">
<fluent-icon icon="location" class="file--icon" size="32" />
</div>
<div class="meta">
<h5 class="text-block-title text-truncate">
{{ name }}
</h5>
<div class="link-wrap">
<a
class="download clear link button small"
rel="noreferrer noopener nofollow"
target="_blank"
:href="mapUrl"
>
{{ $t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP') }}
</a>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
latitude: {
type: Number,
default: undefined,
},
longitude: {
type: Number,
default: undefined,
},
name: {
type: String,
default: '',
},
},
computed: {
mapUrl() {
return `https://maps.google.com/?q=${this.latitude},${this.longitude}`;
},
},
};
</script>
<style lang="scss" scoped>
.location {
display: flex;
flex-direction: row;
padding: var(--space-smaller) 0;
cursor: pointer;
.icon-wrap {
color: var(--s-600);
line-height: 1;
margin: 0 var(--space-smaller);
}
.text-block-title {
margin: 0;
color: var(--s-800);
word-break: break-word;
}
.meta {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-right: var(--space-normal);
}
.link-wrap {
display: flex;
}
}
</style>

View file

@ -17,7 +17,10 @@
@click="snoozeConversation(option.snoozedUntil)" @click="snoozeConversation(option.snoozedUntil)"
/> />
</menu-item-with-submenu> </menu-item-with-submenu>
<menu-item-with-submenu :option="labelMenuConfig"> <menu-item-with-submenu
:option="labelMenuConfig"
:sub-menu-available="!!labels.length"
>
<template> <template>
<menu-item <menu-item
v-for="label in labels" v-for="label in labels"
@ -28,7 +31,10 @@
/> />
</template> </template>
</menu-item-with-submenu> </menu-item-with-submenu>
<menu-item-with-submenu :option="agentMenuConfig"> <menu-item-with-submenu
:option="agentMenuConfig"
:sub-menu-available="!!assignableAgents.length"
>
<agent-loading-placeholder v-if="assignableAgentsUiFlags.isFetching" /> <agent-loading-placeholder v-if="assignableAgentsUiFlags.isFetching" />
<template v-else> <template v-else>
<menu-item <menu-item
@ -40,7 +46,10 @@
/> />
</template> </template>
</menu-item-with-submenu> </menu-item-with-submenu>
<menu-item-with-submenu :option="teamMenuConfig"> <menu-item-with-submenu
:option="teamMenuConfig"
:sub-menu-available="!!teams.length"
>
<menu-item <menu-item
v-for="team in teams" v-for="team in teams"
:key="team.id" :key="team.id"

View file

@ -18,7 +18,7 @@
size="20px" size="20px"
class="agent-thumbnail" class="agent-thumbnail"
/> />
<p class="menu-label truncate-text">{{ option.label }}</p> <p class="menu-label text-truncate">{{ option.label }}</p>
</div> </div>
</template> </template>
@ -50,7 +50,6 @@ export default {
padding: var(--space-smaller); padding: var(--space-smaller);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
overflow: hidden; overflow: hidden;
.menu-label { .menu-label {
margin: 0; margin: 0;
font-size: var(--font-size-mini); font-size: var(--font-size-mini);

View file

@ -1,11 +1,14 @@
<template> <template>
<div class="menu-with-submenu flex-between"> <div
class="menu-with-submenu flex-between"
:class="{ disabled: !subMenuAvailable }"
>
<div class="menu-left"> <div class="menu-left">
<fluent-icon :icon="option.icon" size="14" class="menu-icon" /> <fluent-icon :icon="option.icon" size="14" class="menu-icon" />
<p class="menu-label">{{ option.label }}</p> <p class="menu-label">{{ option.label }}</p>
</div> </div>
<fluent-icon icon="chevron-right" size="12" /> <fluent-icon icon="chevron-right" size="12" />
<div class="submenu"> <div v-if="subMenuAvailable" class="submenu">
<slot /> <slot />
</div> </div>
</div> </div>
@ -18,6 +21,10 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
subMenuAvailable: {
type: Boolean,
default: true,
},
}, },
}; };
</script> </script>
@ -55,6 +62,11 @@ export default {
left: 100%; left: 100%;
top: 0; top: 0;
display: none; display: none;
min-height: min-content;
max-height: var(--space-giga);
overflow-y: auto;
// Need this because Firefox adds a horizontal scrollbar, if a text is truncated inside.
overflow-x: hidden;
} }
&:hover { &:hover {
@ -73,5 +85,10 @@ export default {
clip-path: polygon(100% 0, 0% 0%, 100% 100%); clip-path: polygon(100% 0, 0% 0%, 100% 100%);
} }
} }
&.disabled {
opacity: 50%;
cursor: not-allowed;
}
} }
</style> </style>

View file

@ -0,0 +1,34 @@
import LocationBubble from '../bubble/Location.vue';
export default {
title: 'Components/Help Center',
component: LocationBubble,
argTypes: {
latitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
longitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
name: {
defaultValue: '420, Dope street',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { LocationBubble },
template: '<location-bubble v-bind="$props" />',
});
export const LocationBubbleView = Template.bind({});

View file

@ -0,0 +1,69 @@
import ThumbnailGroup from '../ThumbnailGroup.vue';
export default {
title: 'Components/ThumbnailGroup',
component: ThumbnailGroup,
argTypes: {
usersList: {
defaultValue: [
{
name: 'John',
id: 1,
thumbnail: '',
},
{
name: 'John',
id: 2,
thumbnail: '',
},
{
name: 'John',
id: 3,
thumbnail: '',
},
{
name: 'John',
id: 4,
thumbnail: '',
},
{
name: 'John',
id: 5,
thumbnail: '',
},
{
name: 'John',
id: 6,
thumbnail: '',
},
],
control: {
type: 'object',
},
},
size: {
control: {
type: 'text',
},
},
moreThumbnailsText: {
control: {
type: 'text',
default: '2 more',
},
},
showMoreThumbnailsCount: {
control: {
type: 'boolean',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ThumbnailGroup },
template: '<ThumbnailGroup v-bind="$props"/>',
});
export const Primary = Template.bind({});

View file

@ -25,6 +25,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'notification.created': this.onNotificationCreated, 'notification.created': this.onNotificationCreated,
'first.reply.created': this.onFirstReplyCreated, 'first.reply.created': this.onFirstReplyCreated,
'conversation.read': this.onConversationRead, 'conversation.read': this.onConversationRead,
'conversation.updated': this.onConversationUpdated,
}; };
} }
@ -67,8 +68,7 @@ class ActionCableConnector extends BaseActionCableConnector {
}; };
onConversationRead = data => { onConversationRead = data => {
const { contact_last_seen_at: lastSeen } = data; this.app.$store.dispatch('updateConversation', data);
this.app.$store.dispatch('updateConversationRead', lastSeen);
}; };
onLogout = () => AuthAPI.logout(); onLogout = () => AuthAPI.logout();
@ -85,6 +85,11 @@ class ActionCableConnector extends BaseActionCableConnector {
this.fetchConversationStats(); this.fetchConversationStats();
}; };
onConversationUpdated = data => {
this.app.$store.dispatch('updateConversation', data);
this.fetchConversationStats();
};
onTypingOn = ({ conversation, user }) => { onTypingOn = ({ conversation, user }) => {
const conversationId = conversation.id; const conversationId = conversation.id;

View file

@ -22,7 +22,7 @@ const generatePayload = data => {
let payload = actions.map(item => { let payload = actions.map(item => {
if (Array.isArray(item.action_params)) { if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params); item.action_params = formatArray(item.action_params);
} else if (typeof item.values === 'object') { } else if (typeof item.action_params === 'object') {
item.action_params = [item.action_params.id]; item.action_params = [item.action_params.id];
} else if (!item.action_params) { } else if (!item.action_params) {
item.action_params = []; item.action_params = [];

View file

@ -0,0 +1,242 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_3,
OPERATOR_TYPES_4,
} from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
const MESSAGE_CONDITION_VALUES = [
{
id: 'incoming',
name: 'Incoming Message',
},
{
id: 'outgoing',
name: 'Outgoing Message',
},
];
export const getCustomAttributeInputType = key => {
const customAttributeMap = {
date: 'date',
text: 'plain_text',
list: 'search_select',
checkbox: 'search_select',
};
return customAttributeMap[key] || 'plain_text';
};
export const isACustomAttribute = (customAttributes, key) => {
return customAttributes.find(attr => {
return attr.attribute_key === key;
});
};
export const getCustomAttributeListDropdownValues = (
customAttributes,
type
) => {
return customAttributes
.find(attr => attr.attribute_key === type)
.attribute_values.map(item => {
return {
id: item,
name: item,
};
});
};
export const isCustomAttributeCheckbox = (customAttributes, key) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === key && attr.attribute_display_type === 'checkbox'
);
});
};
export const isCustomAttributeList = (customAttributes, type) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === type && attr.attribute_display_type === 'list'
);
});
};
export const getOperatorTypes = key => {
const operatorMap = {
list: OPERATOR_TYPES_1,
text: OPERATOR_TYPES_3,
number: OPERATOR_TYPES_1,
link: OPERATOR_TYPES_1,
date: OPERATOR_TYPES_4,
checkbox: OPERATOR_TYPES_1,
};
return operatorMap[key] || OPERATOR_TYPES_1;
};
export const generateCustomAttributeTypes = (customAttributes, type) => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
inputType: getCustomAttributeInputType(attr.attribute_display_type),
filterOperators: getOperatorTypes(attr.attribute_display_type),
customAttributeType: type,
};
});
};
export const generateConditionOptions = (options, key = 'id') => {
return options.map(i => {
return {
id: i[key],
name: i.title,
};
});
};
export const getActionOptions = ({ teams, labels, type }) => {
const actionsMap = {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
};
return actionsMap[type];
};
export const getConditionOptions = ({
agents,
booleanFilterOptions,
campaigns,
contacts,
countries,
customAttributes,
inboxes,
languages,
statusFilterOptions,
teams,
type,
}) => {
if (isCustomAttributeCheckbox(customAttributes, type)) {
return booleanFilterOptions;
}
if (isCustomAttributeList(customAttributes, type)) {
return getCustomAttributeListDropdownValues(customAttributes, type);
}
const conditionFilterMaps = {
status: statusFilterOptions,
assignee_id: agents,
contact: contacts,
inbox_id: inboxes,
team_id: teams,
campaigns: generateConditionOptions(campaigns),
browser_language: languages,
country_code: countries,
message_type: MESSAGE_CONDITION_VALUES,
};
return conditionFilterMaps[type];
};
export const getFileName = (action, files = []) => {
const blobId = action.action_params[0];
if (!blobId) return '';
if (action.action_name === 'send_attachment') {
const file = files.find(item => item.blob_id === blobId);
if (file) return file.filename.toString();
}
return '';
};
export const getDefaultConditions = eventName => {
if (eventName === 'message_created') {
return [
{
attribute_key: 'message_type',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
}
return [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
};
export const getDefaultActions = () => {
return [
{
action_name: 'assign_team',
action_params: [],
},
];
};
export const filterCustomAttributes = customAttributes => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
type: attr.attribute_display_type,
};
});
};
export const getStandardAttributeInputType = (automationTypes, event, key) => {
return automationTypes[event].conditions.find(item => item.key === key)
.inputType;
};
export const generateAutomationPayload = payload => {
const automation = JSON.parse(JSON.stringify(payload));
automation.conditions[automation.conditions.length - 1].query_operator = null;
automation.conditions = filterQueryGenerator(automation.conditions).payload;
automation.actions = actionQueryGenerator(automation.actions);
return automation;
};
export const isCustomAttribute = (attrs, key) => {
return attrs.find(attr => attr.key === key);
};
export const generateCustomAttributes = (
conversationAttributes = [],
contactAttribtues = [],
conversationlabel,
contactlabel
) => {
const customAttributes = [];
if (conversationAttributes.length) {
customAttributes.push(
{
key: `conversation_custom_attribute`,
name: conversationlabel,
disabled: true,
},
...conversationAttributes
);
}
if (contactAttribtues.length) {
customAttributes.push(
{
key: `contact_custom_attribute`,
name: contactlabel,
disabled: true,
},
...contactAttribtues
);
}
return customAttributes;
};

View file

@ -1,15 +1,3 @@
const lowerCaseValues = (operator, values) => {
if (operator === 'equal_to' || operator === 'not_equal_to') {
values = values.map(val => {
if (typeof val === 'string') {
return val.toLowerCase();
}
return val;
});
}
return values;
};
const generatePayload = data => { const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues // Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data)); const filters = JSON.parse(JSON.stringify(data));
@ -23,8 +11,6 @@ const generatePayload = data => {
} else { } else {
item.values = [item.values]; item.values = [item.values];
} }
// Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
item.values = lowerCaseValues(item.filter_operator, item.values);
return item; return item;
}); });
// For every query added, the query_operator is set default to and so the // For every query added, the query_operator is set default to and so the

View file

@ -5,7 +5,7 @@ const testData = [
attribute_key: 'status', attribute_key: 'status',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: [ values: [
{ id: 'PENDING', name: 'Pending' }, { id: 'pending', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' }, { id: 'resolved', name: 'Resolved' },
], ],
query_operator: 'and', query_operator: 'and',
@ -18,7 +18,7 @@ const testData = [
account_id: 1, account_id: 1,
auto_offline: true, auto_offline: true,
confirmed: true, confirmed: true,
email: 'fayazara@gmail.com', email: 'fayaz@test.com',
available_name: 'Fayaz', available_name: 'Fayaz',
name: 'Fayaz', name: 'Fayaz',
role: 'agent', role: 'agent',
@ -52,7 +52,7 @@ const finalResult = {
{ {
attribute_key: 'id', attribute_key: 'id',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: ['this is a test'], values: ['This is a test'],
}, },
], ],
}; };

View file

@ -0,0 +1,122 @@
export const teams = [
{
id: 1,
name: '⚙️ sales team',
description: 'This is our internal sales team',
allow_auto_assign: true,
account_id: 1,
is_member: true,
},
{
id: 2,
name: '🤷‍♂️ fayaz',
description: 'Test',
allow_auto_assign: true,
account_id: 1,
is_member: true,
},
{
id: 3,
name: '🇮🇳 apac sales',
description: 'Sales team for France Territory',
allow_auto_assign: true,
account_id: 1,
is_member: true,
},
];
export const labels = [
{
id: 6,
title: 'sales',
description: 'sales team',
color: '#8EA20F',
show_on_sidebar: true,
},
{
id: 2,
title: 'billing',
description: 'billing',
color: '#4077DA',
show_on_sidebar: true,
},
{
id: 1,
title: 'snoozed',
description: 'Items marked for later',
color: '#D12F42',
show_on_sidebar: true,
},
{
id: 5,
title: 'mobile-app',
description: 'tech team',
color: '#2DB1CC',
show_on_sidebar: true,
},
{
id: 14,
title: 'human-resources-department-with-long-title',
description: 'Test',
color: '#FF6E09',
show_on_sidebar: true,
},
{
id: 22,
title: 'priority',
description: 'For important sales leads',
color: '#7E7CED',
show_on_sidebar: true,
},
];
export const agents = [
{
id: 1,
account_id: 1,
availability_status: 'offline',
auto_offline: true,
confirmed: true,
email: 'john@doe.com',
available_name: 'John Doe',
name: 'John Doe',
role: 'agent',
thumbnail:
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--746506837470c1a3dd063e90211ba2386963d52f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/batman_90804.png',
},
{
id: 9,
account_id: 1,
availability_status: 'offline',
auto_offline: true,
confirmed: true,
email: 'clark@kent.com',
available_name: 'Clark Kent',
name: 'Clark Kent',
role: 'agent',
thumbnail: '',
},
];
export const files = [
{
id: 76,
macro_id: 77,
file_type: 'image/jpeg',
account_id: 1,
file_url:
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--aa41b5a779a83c1d86b28475a5cf0bd17f41f0ff/fayaz_cropped.jpeg',
blob_id: 88,
filename: 'fayaz_cropped.jpeg',
},
{
id: 82,
macro_id: 77,
file_type: 'image/png',
account_id: 1,
file_url:
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--260fda80b77409ffaaac10b96681fba447600545/screenshot.png',
blob_id: 94,
filename: 'screenshot.png',
},
];

View file

@ -0,0 +1,75 @@
import {
emptyMacro,
resolveActionName,
resolveLabels,
resolveTeamIds,
getFileName,
resolveAgents,
} from '../../routes/dashboard/settings/macros/macroHelper';
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
import { teams, labels, files, agents } from './macrosFixtures';
describe('#emptyMacro', () => {
const defaultMacro = {
name: '',
actions: [
{
action_name: 'assign_team',
action_params: [],
},
],
visibility: 'global',
};
it('returns the default macro', () => {
expect(emptyMacro).toEqual(defaultMacro);
});
});
describe('#resolveActionName', () => {
it('resolve action name from key and return the correct label', () => {
expect(resolveActionName(MACRO_ACTION_TYPES[0].key)).toEqual(
MACRO_ACTION_TYPES[0].label
);
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).toEqual(
MACRO_ACTION_TYPES[1].label
);
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
MACRO_ACTION_TYPES[0].label
);
});
});
describe('#resolveTeamIds', () => {
it('resolves team names from ids, and returns a joined string', () => {
const resolvedTeams = '⚙️ sales team, 🤷‍♂️ fayaz';
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
});
});
describe('#resolveLabels', () => {
it('resolves labels names from ids and returns a joined string', () => {
const resolvedLabels = 'sales, billing';
expect(resolveLabels(labels, ['sales', 'billing'])).toEqual(resolvedLabels);
});
});
describe('#resolveAgents', () => {
it('resolves agents names from ids and returns a joined string', () => {
const resolvedAgents = 'John Doe';
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
});
});
describe('#getFileName', () => {
it('returns the correct file name from the list of files', () => {
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
files[0].filename
);
expect(getFileName(files[1].blob_id, 'send_attachment', files)).toEqual(
files[1].filename
);
expect(getFileName(files[0].blob_id, 'wrong_action', files)).toEqual('');
expect(getFileName(null, 'send_attachment', files)).toEqual('');
expect(getFileName(files[0].blob_id, 'send_attachment', [])).toEqual('');
});
});

View file

@ -0,0 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
}
}

View file

@ -86,7 +86,9 @@
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه" "RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ" "DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ", "DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "تعذر تحميل المرفق، الرجاء المحاولة مرة أخرى", "UPLOAD_ERROR": "تعذر تحميل المرفق، الرجاء المحاولة مرة أخرى",
"LABEL_IDLE": "ارفع المرفق", "LABEL_IDLE": "ارفع المرفق",
"LABEL_UPLOADING": "جاري الرفع...", "LABEL_UPLOADING": "جاري الرفع...",
"LABEL_UPLOADED": "تم الرفع بنجاح", "LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "فشل الرفع" "LABEL_UPLOAD_FAILED": "فشل الرفع"
} }
} }

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة", "CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
"AGENT_SELECT_LABEL": "اختر وكيل", "AGENT_SELECT_LABEL": "اختر وكيل",
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "العودة للخلف", "GO_BACK_LABEL": "العودة للخلف",
"ASSIGN_LABEL": "تكليف", "ASSIGN_LABEL": "تكليف",
"YES": "نعم",
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل", "ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح", "ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى", "ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",

View file

@ -208,7 +208,8 @@
"CONVERSATION_LABELS": "وسوم المحادثة", "CONVERSATION_LABELS": "وسوم المحادثة",
"CONVERSATION_INFO": "معلومات المحادثة", "CONVERSATION_INFO": "معلومات المحادثة",
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال", "CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
"PREVIOUS_CONVERSATION": "المحادثات السابقة" "PREVIOUS_CONVERSATION": "المحادثات السابقة",
"MACROS": "Macros"
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

View file

@ -54,7 +54,8 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار", "ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف", "ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
"SELECT_ONE": "اختر واحدا" "SELECT_ONE": "اختر واحدا",
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -136,5 +137,8 @@
"UNTIL_NEXT_WEEK": "حتى الأسبوع القادم", "UNTIL_NEXT_WEEK": "حتى الأسبوع القادم",
"UNTIL_TOMORROW": "حتى الغد" "UNTIL_TOMORROW": "حتى الغد"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -217,14 +217,14 @@
"DOMAIN": { "DOMAIN": {
"LABEL": "نطاق مخصص", "LABEL": "نطاق مخصص",
"PLACEHOLDER": "نطاق البوابة المخصص", "PLACEHOLDER": "نطاق البوابة المخصص",
"HELP_TEXT": "أضف فقط إذا كنت ترغب في استخدام نطاق مخصص للبوابات الخاصة بك.", "HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "النطاق المخصص مطلوب" "ERROR": "Enter a valid domain URL"
}, },
"HOME_PAGE_LINK": { "HOME_PAGE_LINK": {
"LABEL": "رابط الصفحة الرئيسية", "LABEL": "رابط الصفحة الرئيسية",
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة", "PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
"HELP_TEXT": "الرابط المستخدم للعودة من البوابة إلى الصفحة الرئيسية.", "HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "رابط الصفحة الرئيسية مطلوب" "ERROR": "Enter a valid home page URL"
}, },
"THEME_COLOR": { "THEME_COLOR": {
"LABEL": "لون قالب البوابة", "LABEL": "لون قالب البوابة",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": { "PUBLISH_ARTICLE": {
"API": { "API": {
"ERROR": "حدث خطأ أثناء نشر المقالة", "ERROR": "حدث خطأ أثناء نشر المقالة",
"SUCCESS": "تم نشر المقالة بنجاح" "SUCCESS": "Article published successfully"
} }
}, },
"ARCHIVE_ARTICLE": { "ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "عنوان Callback URL", "TITLE": "عنوان Callback URL",
"SUBTITLE": "يجب عليك تكوين URL webhook في بوابة مطور فيسبوك مع عنوان URL المذكور هنا." "SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"WEBHOOK_URL": "رابط Webhook",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
}, },
"SUBMIT_BUTTON": "إنشاء قناة واتساب", "SUBMIT_BUTTON": "إنشاء قناة واتساب",
"API": { "API": {
@ -357,7 +359,7 @@
}, },
"FINISH": { "FINISH": {
"TITLE": "أصبحت قناة التواصل جاهزة الآن!", "TITLE": "أصبحت قناة التواصل جاهزة الآن!",
"MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة ", "MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة",
"BUTTON_TEXT": "خذني إلى هناك", "BUTTON_TEXT": "خذني إلى هناك",
"MORE_SETTINGS": "المزيد من الإعدادات", "MORE_SETTINGS": "المزيد من الإعدادات",
"WEBSITE_SUCCESS": "لقد انتهيت بنجاح من إنشاء قناة دردشة مباشرة لموقعك. انسخ الرمز الموضح أدناه وقم بإضافته إلى موقع الويب الخاص بك. في المرة القادمة التي يستخدم فيها العميل الدردشة المباشرة، ستظهر المحادثة تلقائياً على صندوق الوارد الخاص بك." "WEBSITE_SUCCESS": "لقد انتهيت بنجاح من إنشاء قناة دردشة مباشرة لموقعك. انسخ الرمز الموضح أدناه وقم بإضافته إلى موقع الويب الخاص بك. في المرة القادمة التي يستخدم فيها العميل الدردشة المباشرة، ستظهر المحادثة تلقائياً على صندوق الوارد الخاص بك."

View file

@ -0,0 +1,78 @@
{
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
"FORM": {
"NAME": {
"LABEL": "Macro name",
"PLACEHOLDER": "Enter a name for your macro",
"ERROR": "Name is required for creating a macro"
},
"ACTIONS": {
"LABEL": "الإجراءات"
}
},
"API": {
"SUCCESS_MESSAGE": "Macro added successfully",
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
}
},
"LIST": {
"TABLE_HEADER": [
"الاسم",
"Created by",
"Last updated by",
"Visibility"
],
"404": "No macros found"
},
"DELETE": {
"TOOLTIP": "Delete macro",
"CONFIRM": {
"MESSAGE": "هل أنت متأكد من الحذف ",
"YES": "نعم، احذف",
"NO": "لا"
},
"API": {
"SUCCESS_MESSAGE": "Macro deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
}
},
"EDIT": {
"TOOLTIP": "Edit macro",
"API": {
"SUCCESS_MESSAGE": "Macro updated successfully",
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
}
},
"EDITOR": {
"START_FLOW": "Start Flow",
"END_FLOW": "End Flow",
"LOADING": "Fetching macro",
"ADD_BTN_TOOLTIP": "Add new action",
"DELETE_BTN_TOOLTIP": "Delete Action",
"VISIBILITY": {
"LABEL": "Macro Visibility",
"GLOBAL": {
"LABEL": "Public",
"DESCRIPTION": "This macro is available publicly for all agents in this account."
},
"PERSONAL": {
"LABEL": "Private",
"DESCRIPTION": "This macro will be private to you and not be available to others."
}
}
},
"EXECUTE": {
"BUTTON_TOOLTIP": "Execute",
"PREVIEW": "Preview Macro",
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
}
}
}

View file

@ -158,6 +158,9 @@
"DOWNLOAD": "تنزيل", "DOWNLOAD": "تنزيل",
"UPLOADING": "جاري الرفع..." "UPLOADING": "جاري الرفع..."
}, },
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": { "FORM_BUBBLE": {
"SUBMIT": "إرسال" "SUBMIT": "إرسال"
} }
@ -179,6 +182,7 @@
"CONTACTS": "جهات الاتصال", "CONTACTS": "جهات الاتصال",
"HOME": "الرئيسية", "HOME": "الرئيسية",
"AGENTS": "موظف الدعم", "AGENTS": "موظف الدعم",
"AGENT_BOTS": "Bots",
"INBOXES": "قنوات التواصل", "INBOXES": "قنوات التواصل",
"NOTIFICATIONS": "الإشعارات", "NOTIFICATIONS": "الإشعارات",
"CANNED_RESPONSES": "الردود السريعة", "CANNED_RESPONSES": "الردود السريعة",
@ -189,6 +193,7 @@
"LABELS": "الوسوم", "LABELS": "الوسوم",
"CUSTOM_ATTRIBUTES": "سمات مخصصة", "CUSTOM_ATTRIBUTES": "سمات مخصصة",
"AUTOMATION": "الأتمتة", "AUTOMATION": "الأتمتة",
"MACROS": "Macros",
"TEAMS": "الفرق", "TEAMS": "الفرق",
"BILLING": "الفواتير", "BILLING": "الفواتير",
"CUSTOM_VIEWS_FOLDER": "المجلدات", "CUSTOM_VIEWS_FOLDER": "المجلدات",

View file

@ -0,0 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
}
}

View file

@ -86,7 +86,9 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below" "RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save" "DELETE_MESSAGE": "You need to have atleast one condition to save",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save", "DELETE_MESSAGE": "You need to have atleast one action to save",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Could not upload attachment, Please try again", "UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment", "LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "Качване...", "LABEL_UPLOADING": "Качване...",
"LABEL_UPLOADED": "Succesfully Uploaded", "LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed" "LABEL_UPLOAD_FAILED": "Upload Failed"
} }
} }

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent", "AGENT_SELECT_LABEL": "Select Agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign", "ASSIGN_LABEL": "Assign",
"YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

@ -208,7 +208,8 @@
"CONVERSATION_LABELS": "Етикети на разговора", "CONVERSATION_LABELS": "Етикети на разговора",
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Предишни разговори" "PREVIOUS_CONVERSATION": "Предишни разговори",
"MACROS": "Macros"
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

View file

@ -54,7 +54,8 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select", "ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove", "ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one" "SELECT_ONE": "Select one",
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -136,5 +137,8 @@
"UNTIL_NEXT_WEEK": "Until next week", "UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow" "UNTIL_TOMORROW": "Until tomorrow"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -217,14 +217,14 @@
"DOMAIN": { "DOMAIN": {
"LABEL": "Custom Domain", "LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain", "PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.", "HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Custom Domain is required" "ERROR": "Enter a valid domain URL"
}, },
"HOME_PAGE_LINK": { "HOME_PAGE_LINK": {
"LABEL": "Home Page Link", "LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link", "PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.", "HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Home Page Link is required" "ERROR": "Enter a valid home page URL"
}, },
"THEME_COLOR": { "THEME_COLOR": {
"LABEL": "Portal theme color", "LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": { "PUBLISH_ARTICLE": {
"API": { "API": {
"ERROR": "Error while publishing article", "ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully" "SUCCESS": "Article published successfully"
} }
}, },
"ARCHIVE_ARTICLE": { "ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Callback URL", "TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here." "SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"WEBHOOK_URL": "Webhook URL",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
}, },
"SUBMIT_BUTTON": "Create WhatsApp Channel", "SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": { "API": {
@ -357,7 +359,7 @@
}, },
"FINISH": { "FINISH": {
"TITLE": "Your Inbox is ready!", "TITLE": "Your Inbox is ready!",
"MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting ", "MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting",
"BUTTON_TEXT": "Take me there", "BUTTON_TEXT": "Take me there",
"MORE_SETTINGS": "More settings", "MORE_SETTINGS": "More settings",
"WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox." "WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox."

View file

@ -0,0 +1,78 @@
{
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
"FORM": {
"NAME": {
"LABEL": "Macro name",
"PLACEHOLDER": "Enter a name for your macro",
"ERROR": "Name is required for creating a macro"
},
"ACTIONS": {
"LABEL": "Действия"
}
},
"API": {
"SUCCESS_MESSAGE": "Macro added successfully",
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
}
},
"LIST": {
"TABLE_HEADER": [
"Име",
"Created by",
"Last updated by",
"Visibility"
],
"404": "No macros found"
},
"DELETE": {
"TOOLTIP": "Delete macro",
"CONFIRM": {
"MESSAGE": "Сигурни ли сте за изтриването ",
"YES": "Да, изтрий",
"NO": "No"
},
"API": {
"SUCCESS_MESSAGE": "Macro deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
}
},
"EDIT": {
"TOOLTIP": "Edit macro",
"API": {
"SUCCESS_MESSAGE": "Macro updated successfully",
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
}
},
"EDITOR": {
"START_FLOW": "Start Flow",
"END_FLOW": "End Flow",
"LOADING": "Fetching macro",
"ADD_BTN_TOOLTIP": "Add new action",
"DELETE_BTN_TOOLTIP": "Delete Action",
"VISIBILITY": {
"LABEL": "Macro Visibility",
"GLOBAL": {
"LABEL": "Public",
"DESCRIPTION": "This macro is available publicly for all agents in this account."
},
"PERSONAL": {
"LABEL": "Private",
"DESCRIPTION": "This macro will be private to you and not be available to others."
}
}
},
"EXECUTE": {
"BUTTON_TOOLTIP": "Execute",
"PREVIEW": "Preview Macro",
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
}
}
}

View file

@ -158,6 +158,9 @@
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"UPLOADING": "Качване..." "UPLOADING": "Качване..."
}, },
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": { "FORM_BUBBLE": {
"SUBMIT": "Изпращане" "SUBMIT": "Изпращане"
} }
@ -179,6 +182,7 @@
"CONTACTS": "Контакти", "CONTACTS": "Контакти",
"HOME": "Home", "HOME": "Home",
"AGENTS": "Агенти", "AGENTS": "Агенти",
"AGENT_BOTS": "Bots",
"INBOXES": "Inboxes", "INBOXES": "Inboxes",
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notifications",
"CANNED_RESPONSES": "Готови отговори", "CANNED_RESPONSES": "Готови отговори",
@ -189,6 +193,7 @@
"LABELS": "Labels", "LABELS": "Labels",
"CUSTOM_ATTRIBUTES": "Персонализирани атрибути", "CUSTOM_ATTRIBUTES": "Персонализирани атрибути",
"AUTOMATION": "Автоматизация", "AUTOMATION": "Автоматизация",
"MACROS": "Macros",
"TEAMS": "Teams", "TEAMS": "Teams",
"BILLING": "Billing", "BILLING": "Billing",
"CUSTOM_VIEWS_FOLDER": "Folders", "CUSTOM_VIEWS_FOLDER": "Folders",

View file

@ -1,48 +1,48 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "Filter Conversations", "TITLE": "Filtre de converses",
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.", "SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.",
"ADD_NEW_FILTER": "Add Filter", "ADD_NEW_FILTER": "Afegeix filtre",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save", "FILTER_DELETE_ERROR": "You should have atleast one filter to save",
"SUBMIT_BUTTON_LABEL": "Apply filters", "SUBMIT_BUTTON_LABEL": "Aplicar filtres",
"CANCEL_BUTTON_LABEL": "Cancel·la", "CANCEL_BUTTON_LABEL": "Cancel·la",
"CLEAR_BUTTON_LABEL": "Clear Filters", "CLEAR_BUTTON_LABEL": "Clear Filters",
"EMPTY_VALUE_ERROR": "Value is required", "EMPTY_VALUE_ERROR": "El valor és necessari",
"TOOLTIP_LABEL": "Filter conversations", "TOOLTIP_LABEL": "Filtre de converses",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "AND", "AND": "I",
"OR": "OR" "OR": "O"
}, },
"OPERATOR_LABELS": { "OPERATOR_LABELS": {
"equal_to": "Equal to", "equal_to": "Igual a",
"not_equal_to": "Not equal to", "not_equal_to": "No és igual a",
"contains": "Contains", "contains": "Conté",
"does_not_contain": "Does not contain", "does_not_contain": "No conté",
"is_present": "Is present", "is_present": "És present",
"is_not_present": "Is not present", "is_not_present": "No és present",
"is_greater_than": "Is greater than", "is_greater_than": "És més gran que",
"is_less_than": "Is lesser than", "is_less_than": "És més petit que",
"days_before": "Is x days before" "days_before": "Is x days before"
}, },
"ATTRIBUTE_LABELS": { "ATTRIBUTE_LABELS": {
"TRUE": "True", "TRUE": "Cert",
"FALSE": "False" "FALSE": "Fals"
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"STATUS": "Estat", "STATUS": "Estat",
"ASSIGNEE_NAME": "Assignee Name", "ASSIGNEE_NAME": "Assignee Name",
"INBOX_NAME": "Nom de la safata d'entrada", "INBOX_NAME": "Nom de la safata d'entrada",
"TEAM_NAME": "Team Name", "TEAM_NAME": "Nom de l'equip",
"CONVERSATION_IDENTIFIER": "Conversation Identifier", "CONVERSATION_IDENTIFIER": "Identificador de la conversa",
"CAMPAIGN_NAME": "Campaign Name", "CAMPAIGN_NAME": "Campaign Name",
"LABELS": "Etiquetes", "LABELS": "Etiquetes",
"BROWSER_LANGUAGE": "Browser Language", "BROWSER_LANGUAGE": "Browser Language",
"COUNTRY_NAME": "Country Name", "COUNTRY_NAME": "Nom del país",
"REFERER_LINK": "Referer link", "REFERER_LINK": "Referer link",
"CUSTOM_ATTRIBUTE_LIST": "List", "CUSTOM_ATTRIBUTE_LIST": "List",
"CUSTOM_ATTRIBUTE_TEXT": "Text", "CUSTOM_ATTRIBUTE_TEXT": "Llista",
"CUSTOM_ATTRIBUTE_NUMBER": "Number", "CUSTOM_ATTRIBUTE_NUMBER": "Número",
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Enllaç",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity" "LAST_ACTIVITY": "Last Activity"

View file

@ -0,0 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
}
}

View file

@ -96,16 +96,16 @@
"PLACEHOLDER": "Ningú", "PLACEHOLDER": "Ningú",
"TITLE": { "TITLE": {
"AGENT": "Seleccionar Agent", "AGENT": "Seleccionar Agent",
"TEAM": "Select team" "TEAM": "Selecciona equip"
}, },
"SEARCH": { "SEARCH": {
"NO_RESULTS": { "NO_RESULTS": {
"AGENT": "No s'han trobat agents", "AGENT": "No s'han trobat agents",
"TEAM": "No teams found" "TEAM": "No s'han trobat equips"
}, },
"PLACEHOLDER": { "PLACEHOLDER": {
"AGENT": "Search agents", "AGENT": "Cerca agents",
"TEAM": "Search teams" "TEAM": "Cerca equips"
} }
} }
} }

View file

@ -20,7 +20,7 @@
"ERROR": "Description is required" "ERROR": "Description is required"
}, },
"EVENT": { "EVENT": {
"LABEL": "Event", "LABEL": "Esdeveniment",
"PLACEHOLDER": "Please select one", "PLACEHOLDER": "Please select one",
"ERROR": "Event is required" "ERROR": "Event is required"
}, },
@ -86,7 +86,9 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below" "RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save" "DELETE_MESSAGE": "You need to have atleast one condition to save",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save", "DELETE_MESSAGE": "You need to have atleast one action to save",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Could not upload attachment, Please try again", "UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment", "LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "S'està carregant...", "LABEL_UPLOADING": "S'està carregant...",
"LABEL_UPLOADED": "Succesfully Uploaded", "LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed" "LABEL_UPLOAD_FAILED": "Upload Failed"
} }
} }

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Seleccionar Agent", "AGENT_SELECT_LABEL": "Seleccionar Agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assignar", "ASSIGN_LABEL": "Assignar",
"YES": "Si",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

@ -22,7 +22,7 @@
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Codi curt", "LABEL": "Codi curt",
"PLACEHOLDER": "Please enter a short code", "PLACEHOLDER": "Introduïu un codi curt",
"ERROR": "És necessari el codi curt" "ERROR": "És necessari el codi curt"
}, },
"CONTENT": { "CONTENT": {

View file

@ -7,7 +7,7 @@
"404": "No hi ha converses actives en aquest grup." "404": "No hi ha converses actives en aquest grup."
}, },
"TAB_HEADING": "Converses", "TAB_HEADING": "Converses",
"MENTION_HEADING": "Mentions", "MENTION_HEADING": "Mencions",
"SEARCH": { "SEARCH": {
"INPUT": "Cerca persones, xats, respostes desades .." "INPUT": "Cerca persones, xats, respostes desades .."
}, },
@ -25,10 +25,10 @@
"TEXT": "Resoltes" "TEXT": "Resoltes"
}, },
"pending": { "pending": {
"TEXT": "Pending" "TEXT": "Pendent"
}, },
"snoozed": { "snoozed": {
"TEXT": "Snoozed" "TEXT": "Posposat"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {
@ -54,12 +54,12 @@
"RECEIVED_VIA_EMAIL": "Rebut per correu electrònic", "RECEIVED_VIA_EMAIL": "Rebut per correu electrònic",
"VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter", "VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter",
"REPLY_TO_TWEET": "Respon a aquest tuit", "REPLY_TO_TWEET": "Respon a aquest tuit",
"LINK_TO_STORY": "Go to instagram story", "LINK_TO_STORY": "Ves a la història d'instagram",
"SENT": "Sent successfully", "SENT": "Enviat correctament",
"NO_MESSAGES": "Cap Missatge", "NO_MESSAGES": "Cap Missatge",
"NO_CONTENT": "No content available", "NO_CONTENT": "No hi ha contingut disponible",
"HIDE_QUOTED_TEXT": "Hide Quoted Text", "HIDE_QUOTED_TEXT": "Amaga text entre cometes",
"SHOW_QUOTED_TEXT": "Show Quoted Text", "SHOW_QUOTED_TEXT": "Mostra text entre cometes",
"MESSAGE_READ": "Read" "MESSAGE_READ": "Llegir"
} }
} }

View file

@ -3,40 +3,40 @@
"NOT_AVAILABLE": "No disponible", "NOT_AVAILABLE": "No disponible",
"EMAIL_ADDRESS": "Adreça de correu electrònic", "EMAIL_ADDRESS": "Adreça de correu electrònic",
"PHONE_NUMBER": "Número de telèfon", "PHONE_NUMBER": "Número de telèfon",
"IDENTIFIER": "Identifier", "IDENTIFIER": "Identificador",
"COPY_SUCCESSFUL": "S'ha copiat al porta-retalls amb èxit", "COPY_SUCCESSFUL": "S'ha copiat al porta-retalls amb èxit",
"COMPANY": "Companyia", "COMPANY": "Companyia",
"LOCATION": "Ubicació", "LOCATION": "Ubicació",
"BROWSER_LANGUAGE": "Browser Language", "BROWSER_LANGUAGE": "Idioma del navegador",
"CONVERSATION_TITLE": "Detalls de les converses", "CONVERSATION_TITLE": "Detalls de les converses",
"VIEW_PROFILE": "View Profile", "VIEW_PROFILE": "Veure perfil",
"BROWSER": "Navegador", "BROWSER": "Navegador",
"OS": "Sistema operatiu", "OS": "Sistema operatiu",
"INITIATED_FROM": "Iniciada des de", "INITIATED_FROM": "Iniciada des de",
"INITIATED_AT": "Iniciada a les", "INITIATED_AT": "Iniciada a les",
"IP_ADDRESS": "Adreça IP", "IP_ADDRESS": "Adreça IP",
"NEW_MESSAGE": "New message", "NEW_MESSAGE": "Nou missatge",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "No hi han converses prèvies associades a aquest contacte.", "NO_RECORDS_FOUND": "No hi han converses prèvies associades a aquest contacte.",
"TITLE": "Converses prèvies" "TITLE": "Converses prèvies"
}, },
"LABELS": { "LABELS": {
"CONTACT": { "CONTACT": {
"TITLE": "Contact Labels", "TITLE": "Etiquetes de contactes",
"ERROR": "Couldn't update labels" "ERROR": "No s'han pogut actualitzar les etiquetes"
}, },
"CONVERSATION": { "CONVERSATION": {
"TITLE": "Etiquetes de converses", "TITLE": "Etiquetes de converses",
"ADD_BUTTON": "Add Labels" "ADD_BUTTON": "Afegir etiquetes"
}, },
"LABEL_SELECT": { "LABEL_SELECT": {
"TITLE": "Add Labels", "TITLE": "Afegir etiquetes",
"PLACEHOLDER": "Search labels", "PLACEHOLDER": "Cerca etiquetes",
"NO_RESULT": "No labels found" "NO_RESULT": "No s'han trobat etiquetes"
} }
}, },
"MERGE_CONTACT": "Merge contact", "MERGE_CONTACT": "Reagrupa contacte",
"CONTACT_ACTIONS": "Contact actions", "CONTACT_ACTIONS": "Accions de contacte",
"MUTE_CONTACT": "Silencia la conversa", "MUTE_CONTACT": "Silencia la conversa",
"UNMUTE_CONTACT": "Desactiva el silenci de la conversa", "UNMUTE_CONTACT": "Desactiva el silenci de la conversa",
"MUTED_SUCCESS": "Aquesta conversa s'ha silenciat durant 6 hores", "MUTED_SUCCESS": "Aquesta conversa s'ha silenciat durant 6 hores",
@ -45,7 +45,7 @@
"EDIT_LABEL": "Edita", "EDIT_LABEL": "Edita",
"SIDEBAR_SECTIONS": { "SIDEBAR_SECTIONS": {
"CUSTOM_ATTRIBUTES": "Atributs personalitzats", "CUSTOM_ATTRIBUTES": "Atributs personalitzats",
"CONTACT_LABELS": "Contact Labels", "CONTACT_LABELS": "Etiquetes de contactes",
"PREVIOUS_CONVERSATIONS": "Converses prèvies" "PREVIOUS_CONVERSATIONS": "Converses prèvies"
} }
}, },
@ -60,30 +60,30 @@
"DESC": "Afegir informació bàsica sobre el contacte." "DESC": "Afegir informació bàsica sobre el contacte."
}, },
"IMPORT_CONTACTS": { "IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import", "BUTTON_LABEL": "Importa",
"TITLE": "Import Contacts", "TITLE": "Importa contactes",
"DESC": "Import contacts through a CSV file.", "DESC": "Importa contactes a través d'un fitxer CSV.",
"DOWNLOAD_LABEL": "Download a sample csv.", "DOWNLOAD_LABEL": "Descarrega un csv d'exemple.",
"FORM": { "FORM": {
"LABEL": "CSV File", "LABEL": "Fitxer CSV",
"SUBMIT": "Import", "SUBMIT": "Importa",
"CANCEL": "Cancel·la" "CANCEL": "Cancel·la"
}, },
"SUCCESS_MESSAGE": "Contacts saved successfully", "SUCCESS_MESSAGE": "Contactes desat correctament",
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar" "ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
}, },
"DELETE_NOTE": { "DELETE_NOTE": {
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirma l'esborrat", "TITLE": "Confirma l'esborrat",
"MESSAGE": "Are you want sure to delete this note?", "MESSAGE": "Vols suprimir aquesta nota amb seguretat?",
"YES": "Yes, Delete it", "YES": "Si, esborra'l",
"NO": "No, manten-la" "NO": "No, manten-la"
} }
}, },
"DELETE_CONTACT": { "DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact", "BUTTON_LABEL": "Contacte esborrat",
"TITLE": "Delete contact", "TITLE": "Contacte esborrat",
"DESC": "Delete contact details", "DESC": "Detalls del contacte esborrat",
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirma l'esborrat", "TITLE": "Confirma l'esborrat",
"MESSAGE": "N'estas segur? ", "MESSAGE": "N'estas segur? ",
@ -91,8 +91,8 @@
"NO": "No, segueix" "NO": "No, segueix"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Contact deleted successfully", "SUCCESS_MESSAGE": "Contacte esborrat correctament",
"ERROR_MESSAGE": "Could not delete contact. Please try again later." "ERROR_MESSAGE": "No s'ha pogut esborrar el contacte. Torneu-ho a provar."
} }
}, },
"CONTACT_FORM": { "CONTACT_FORM": {
@ -160,7 +160,7 @@
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar" "ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
}, },
"NEW_CONVERSATION": { "NEW_CONVERSATION": {
"BUTTON_LABEL": "Start conversation", "BUTTON_LABEL": "Inicia la conversa",
"TITLE": "Nova conversació", "TITLE": "Nova conversació",
"DESC": "Start a new conversation by sending a new message.", "DESC": "Start a new conversation by sending a new message.",
"NO_INBOX": "Couldn't find an inbox to initiate a new conversation with this contact.", "NO_INBOX": "Couldn't find an inbox to initiate a new conversation with this contact.",

View file

@ -2,26 +2,26 @@
"CONTACTS_FILTER": { "CONTACTS_FILTER": {
"TITLE": "Filter Contacts", "TITLE": "Filter Contacts",
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.", "SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
"ADD_NEW_FILTER": "Add Filter", "ADD_NEW_FILTER": "Afegeix filtre",
"CLEAR_ALL_FILTERS": "Clear All Filters", "CLEAR_ALL_FILTERS": "Clear All Filters",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save", "FILTER_DELETE_ERROR": "You should have atleast one filter to save",
"SUBMIT_BUTTON_LABEL": "Envia", "SUBMIT_BUTTON_LABEL": "Envia",
"CANCEL_BUTTON_LABEL": "Cancel·la", "CANCEL_BUTTON_LABEL": "Cancel·la",
"CLEAR_BUTTON_LABEL": "Clear Filters", "CLEAR_BUTTON_LABEL": "Clear Filters",
"EMPTY_VALUE_ERROR": "Value is required", "EMPTY_VALUE_ERROR": "El valor és necessari",
"TOOLTIP_LABEL": "Filter contacts", "TOOLTIP_LABEL": "Filter contacts",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "AND", "AND": "I",
"OR": "OR" "OR": "O"
}, },
"OPERATOR_LABELS": { "OPERATOR_LABELS": {
"equal_to": "Equal to", "equal_to": "Igual a",
"not_equal_to": "Not equal to", "not_equal_to": "No és igual a",
"contains": "Contains", "contains": "Conté",
"does_not_contain": "Does not contain", "does_not_contain": "No conté",
"is_present": "Is present", "is_present": "És present",
"is_not_present": "Is not present", "is_not_present": "No és present",
"is_greater_than": "Is greater than", "is_greater_than": "És més gran que",
"is_lesser_than": "Is lesser than", "is_lesser_than": "Is lesser than",
"days_before": "Is x days before" "days_before": "Is x days before"
}, },
@ -33,9 +33,9 @@
"CITY": "City", "CITY": "City",
"COUNTRY": "Country", "COUNTRY": "Country",
"CUSTOM_ATTRIBUTE_LIST": "List", "CUSTOM_ATTRIBUTE_LIST": "List",
"CUSTOM_ATTRIBUTE_TEXT": "Text", "CUSTOM_ATTRIBUTE_TEXT": "Llista",
"CUSTOM_ATTRIBUTE_NUMBER": "Number", "CUSTOM_ATTRIBUTE_NUMBER": "Número",
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Enllaç",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity", "LAST_ACTIVITY": "Last Activity",

View file

@ -208,7 +208,8 @@
"CONVERSATION_LABELS": "Etiquetes de converses", "CONVERSATION_LABELS": "Etiquetes de converses",
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Converses prèvies" "PREVIOUS_CONVERSATION": "Converses prèvies",
"MACROS": "Macros"
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

View file

@ -54,7 +54,8 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Presiona retorn (tecla enter) per seleccionar", "ENTER_TO_SELECT": "Presiona retorn (tecla enter) per seleccionar",
"ENTER_TO_REMOVE": "Presiona retorn (tecla enter) per eliminar", "ENTER_TO_REMOVE": "Presiona retorn (tecla enter) per eliminar",
"SELECT_ONE": "Selecciona un" "SELECT_ONE": "Selecciona un",
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -136,5 +137,8 @@
"UNTIL_NEXT_WEEK": "Until next week", "UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow" "UNTIL_TOMORROW": "Until tomorrow"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -217,14 +217,14 @@
"DOMAIN": { "DOMAIN": {
"LABEL": "Custom Domain", "LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain", "PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.", "HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Custom Domain is required" "ERROR": "Enter a valid domain URL"
}, },
"HOME_PAGE_LINK": { "HOME_PAGE_LINK": {
"LABEL": "Home Page Link", "LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link", "PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.", "HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Home Page Link is required" "ERROR": "Enter a valid home page URL"
}, },
"THEME_COLOR": { "THEME_COLOR": {
"LABEL": "Portal theme color", "LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": { "PUBLISH_ARTICLE": {
"API": { "API": {
"ERROR": "Error while publishing article", "ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully" "SUCCESS": "Article published successfully"
} }
}, },
"ARCHIVE_ARTICLE": { "ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Callback URL", "TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here." "SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"WEBHOOK_URL": "URL del webhook",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
}, },
"SUBMIT_BUTTON": "Create WhatsApp Channel", "SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": { "API": {
@ -357,7 +359,7 @@
}, },
"FINISH": { "FINISH": {
"TITLE": "La vostra safata d'entrada està a punt!", "TITLE": "La vostra safata d'entrada està a punt!",
"MESSAGE": "Ja podeu interactuar amb els vostres clients a través del vostre canal nou. Feliç suport ", "MESSAGE": "Ja podeu interactuar amb els vostres clients a través del vostre canal nou. Feliç suport",
"BUTTON_TEXT": "Porta'm allà", "BUTTON_TEXT": "Porta'm allà",
"MORE_SETTINGS": "More settings", "MORE_SETTINGS": "More settings",
"WEBSITE_SUCCESS": "Heu finalitzat amb èxit la creació d'un canal web. Copieu el codi que es mostra a continuació i enganxeu-lo al lloc web. La propera vegada que un client utilitzi el xat en directe, la conversa apareixerà automàticament a la safata d'entrada." "WEBSITE_SUCCESS": "Heu finalitzat amb èxit la creació d'un canal web. Copieu el codi que es mostra a continuació i enganxeu-lo al lloc web. La propera vegada que un client utilitzi el xat en directe, la conversa apareixerà automàticament a la safata d'entrada."

View file

@ -0,0 +1,78 @@
{
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
"FORM": {
"NAME": {
"LABEL": "Macro name",
"PLACEHOLDER": "Enter a name for your macro",
"ERROR": "Name is required for creating a macro"
},
"ACTIONS": {
"LABEL": "Accions"
}
},
"API": {
"SUCCESS_MESSAGE": "Macro added successfully",
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
}
},
"LIST": {
"TABLE_HEADER": [
"Nom",
"Created by",
"Last updated by",
"Visibility"
],
"404": "No macros found"
},
"DELETE": {
"TOOLTIP": "Delete macro",
"CONFIRM": {
"MESSAGE": "N'estas segur? ",
"YES": "Si, esborra",
"NO": "No"
},
"API": {
"SUCCESS_MESSAGE": "Macro deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
}
},
"EDIT": {
"TOOLTIP": "Edit macro",
"API": {
"SUCCESS_MESSAGE": "Macro updated successfully",
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
}
},
"EDITOR": {
"START_FLOW": "Start Flow",
"END_FLOW": "End Flow",
"LOADING": "Fetching macro",
"ADD_BTN_TOOLTIP": "Add new action",
"DELETE_BTN_TOOLTIP": "Delete Action",
"VISIBILITY": {
"LABEL": "Macro Visibility",
"GLOBAL": {
"LABEL": "Public",
"DESCRIPTION": "This macro is available publicly for all agents in this account."
},
"PERSONAL": {
"LABEL": "Private",
"DESCRIPTION": "This macro will be private to you and not be available to others."
}
}
},
"EXECUTE": {
"BUTTON_TOOLTIP": "Execute",
"PREVIEW": "Preview Macro",
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
}
}
}

View file

@ -158,6 +158,9 @@
"DOWNLOAD": "Descarrega", "DOWNLOAD": "Descarrega",
"UPLOADING": "S'està carregant..." "UPLOADING": "S'està carregant..."
}, },
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": { "FORM_BUBBLE": {
"SUBMIT": "Envia" "SUBMIT": "Envia"
} }
@ -173,12 +176,13 @@
"SWITCH": "Switch", "SWITCH": "Switch",
"CONVERSATIONS": "Converses", "CONVERSATIONS": "Converses",
"ALL_CONVERSATIONS": "All Conversations", "ALL_CONVERSATIONS": "All Conversations",
"MENTIONED_CONVERSATIONS": "Mentions", "MENTIONED_CONVERSATIONS": "Mencions",
"REPORTS": "Informes", "REPORTS": "Informes",
"SETTINGS": "Configuracions", "SETTINGS": "Configuracions",
"CONTACTS": "Contactes", "CONTACTS": "Contactes",
"HOME": "Inici", "HOME": "Inici",
"AGENTS": "Agents", "AGENTS": "Agents",
"AGENT_BOTS": "Bots",
"INBOXES": "Safates d'entrada", "INBOXES": "Safates d'entrada",
"NOTIFICATIONS": "Notificacions", "NOTIFICATIONS": "Notificacions",
"CANNED_RESPONSES": "Respostes predeterminades", "CANNED_RESPONSES": "Respostes predeterminades",
@ -189,14 +193,15 @@
"LABELS": "Etiquetes", "LABELS": "Etiquetes",
"CUSTOM_ATTRIBUTES": "Atributs personalitzats", "CUSTOM_ATTRIBUTES": "Atributs personalitzats",
"AUTOMATION": "Automation", "AUTOMATION": "Automation",
"MACROS": "Macros",
"TEAMS": "Equips", "TEAMS": "Equips",
"BILLING": "Billing", "BILLING": "Billing",
"CUSTOM_VIEWS_FOLDER": "Folders", "CUSTOM_VIEWS_FOLDER": "Folders",
"CUSTOM_VIEWS_SEGMENTS": "Segments", "CUSTOM_VIEWS_SEGMENTS": "Segments",
"ALL_CONTACTS": "All Contacts", "ALL_CONTACTS": "All Contacts",
"TAGGED_WITH": "Tagged with", "TAGGED_WITH": "Tagged with",
"NEW_LABEL": "New label", "NEW_LABEL": "Nova etiqueta",
"NEW_TEAM": "New team", "NEW_TEAM": "Nou equip",
"NEW_INBOX": "New inbox", "NEW_INBOX": "New inbox",
"REPORTS_CONVERSATION": "Converses", "REPORTS_CONVERSATION": "Converses",
"CSAT": "CSAT", "CSAT": "CSAT",

View file

@ -0,0 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
}
}

View file

@ -86,7 +86,9 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below" "RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save" "DELETE_MESSAGE": "You need to have atleast one condition to save",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save", "DELETE_MESSAGE": "You need to have atleast one action to save",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Could not upload attachment, Please try again", "UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment", "LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "Nahrávání...", "LABEL_UPLOADING": "Nahrávání...",
"LABEL_UPLOADED": "Succesfully Uploaded", "LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed" "LABEL_UPLOAD_FAILED": "Upload Failed"
} }
} }

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Vybrat agenta", "AGENT_SELECT_LABEL": "Vybrat agenta",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Přiřadit", "ASSIGN_LABEL": "Přiřadit",
"YES": "Ano",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

@ -208,7 +208,8 @@
"CONVERSATION_LABELS": "Štítky konverzace", "CONVERSATION_LABELS": "Štítky konverzace",
"CONVERSATION_INFO": "Informace o konverzaci", "CONVERSATION_INFO": "Informace o konverzaci",
"CONTACT_ATTRIBUTES": "Atributy kontaktu", "CONTACT_ATTRIBUTES": "Atributy kontaktu",
"PREVIOUS_CONVERSATION": "Předchozí konverzace" "PREVIOUS_CONVERSATION": "Předchozí konverzace",
"MACROS": "Macros"
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

View file

@ -54,7 +54,8 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Stiskněte Enter pro vybrání", "ENTER_TO_SELECT": "Stiskněte Enter pro vybrání",
"ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání", "ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
"SELECT_ONE": "Vyberte jeden" "SELECT_ONE": "Vyberte jeden",
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -136,5 +137,8 @@
"UNTIL_NEXT_WEEK": "Until next week", "UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow" "UNTIL_TOMORROW": "Until tomorrow"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -217,14 +217,14 @@
"DOMAIN": { "DOMAIN": {
"LABEL": "Custom Domain", "LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain", "PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.", "HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Custom Domain is required" "ERROR": "Enter a valid domain URL"
}, },
"HOME_PAGE_LINK": { "HOME_PAGE_LINK": {
"LABEL": "Home Page Link", "LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link", "PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.", "HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Home Page Link is required" "ERROR": "Enter a valid home page URL"
}, },
"THEME_COLOR": { "THEME_COLOR": {
"LABEL": "Portal theme color", "LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": { "PUBLISH_ARTICLE": {
"API": { "API": {
"ERROR": "Error while publishing article", "ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully" "SUCCESS": "Article published successfully"
} }
}, },
"ARCHIVE_ARTICLE": { "ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Callback URL", "TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here." "SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"WEBHOOK_URL": "URL webového háčku",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
}, },
"SUBMIT_BUTTON": "Create WhatsApp Channel", "SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": { "API": {
@ -357,7 +359,7 @@
}, },
"FINISH": { "FINISH": {
"TITLE": "Vaše doručená pošta je připravena!", "TITLE": "Vaše doručená pošta je připravena!",
"MESSAGE": "Nyní se můžete spojit se svými zákazníky prostřednictvím nového kanálu. Šťastná podpora ", "MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting",
"BUTTON_TEXT": "Vezmi mě tam", "BUTTON_TEXT": "Vezmi mě tam",
"MORE_SETTINGS": "More settings", "MORE_SETTINGS": "More settings",
"WEBSITE_SUCCESS": "Úspěšně jste dokončili vytvoření webového kanálu. Zkopírujte kód zobrazený níže a vložte jej na vaše webové stránky. Když zákazník příště použije živý chat, konverzace se automaticky objeví ve vaší doručené poště." "WEBSITE_SUCCESS": "Úspěšně jste dokončili vytvoření webového kanálu. Zkopírujte kód zobrazený níže a vložte jej na vaše webové stránky. Když zákazník příště použije živý chat, konverzace se automaticky objeví ve vaší doručené poště."

View file

@ -0,0 +1,78 @@
{
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
"FORM": {
"NAME": {
"LABEL": "Macro name",
"PLACEHOLDER": "Enter a name for your macro",
"ERROR": "Name is required for creating a macro"
},
"ACTIONS": {
"LABEL": "Akce"
}
},
"API": {
"SUCCESS_MESSAGE": "Macro added successfully",
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
}
},
"LIST": {
"TABLE_HEADER": [
"Název",
"Created by",
"Last updated by",
"Visibility"
],
"404": "No macros found"
},
"DELETE": {
"TOOLTIP": "Delete macro",
"CONFIRM": {
"MESSAGE": "Opravdu chcete odstranit ",
"YES": "Ano, odstranit",
"NO": "Ne"
},
"API": {
"SUCCESS_MESSAGE": "Macro deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
}
},
"EDIT": {
"TOOLTIP": "Edit macro",
"API": {
"SUCCESS_MESSAGE": "Macro updated successfully",
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
}
},
"EDITOR": {
"START_FLOW": "Start Flow",
"END_FLOW": "End Flow",
"LOADING": "Fetching macro",
"ADD_BTN_TOOLTIP": "Add new action",
"DELETE_BTN_TOOLTIP": "Delete Action",
"VISIBILITY": {
"LABEL": "Macro Visibility",
"GLOBAL": {
"LABEL": "Public",
"DESCRIPTION": "This macro is available publicly for all agents in this account."
},
"PERSONAL": {
"LABEL": "Private",
"DESCRIPTION": "This macro will be private to you and not be available to others."
}
}
},
"EXECUTE": {
"BUTTON_TOOLTIP": "Execute",
"PREVIEW": "Preview Macro",
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
}
}
}

View file

@ -158,6 +158,9 @@
"DOWNLOAD": "Stáhnout", "DOWNLOAD": "Stáhnout",
"UPLOADING": "Nahrávání..." "UPLOADING": "Nahrávání..."
}, },
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": { "FORM_BUBBLE": {
"SUBMIT": "Odeslat" "SUBMIT": "Odeslat"
} }
@ -179,6 +182,7 @@
"CONTACTS": "Kontakty", "CONTACTS": "Kontakty",
"HOME": "Domů", "HOME": "Domů",
"AGENTS": "Agenti", "AGENTS": "Agenti",
"AGENT_BOTS": "Bots",
"INBOXES": "Schránky", "INBOXES": "Schránky",
"NOTIFICATIONS": "Oznámení", "NOTIFICATIONS": "Oznámení",
"CANNED_RESPONSES": "Konzervované odpovědi", "CANNED_RESPONSES": "Konzervované odpovědi",
@ -189,6 +193,7 @@
"LABELS": "Štítky", "LABELS": "Štítky",
"CUSTOM_ATTRIBUTES": "Vlastní atributy", "CUSTOM_ATTRIBUTES": "Vlastní atributy",
"AUTOMATION": "Automation", "AUTOMATION": "Automation",
"MACROS": "Macros",
"TEAMS": "Týmy", "TEAMS": "Týmy",
"BILLING": "Billing", "BILLING": "Billing",
"CUSTOM_VIEWS_FOLDER": "Folders", "CUSTOM_VIEWS_FOLDER": "Folders",

View file

@ -0,0 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
}
}

View file

@ -86,7 +86,9 @@
"RESET_MESSAGE": "Ændring af begivenhedstype vil nulstille de betingelser og begivenheder, du har tilføjet nedenfor" "RESET_MESSAGE": "Ændring af begivenhedstype vil nulstille de betingelser og begivenheder, du har tilføjet nedenfor"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "Du skal have mindst én betingelse for at gemme" "DELETE_MESSAGE": "Du skal have mindst én betingelse for at gemme",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "Du skal have mindst én handling for at gemme", "DELETE_MESSAGE": "Du skal have mindst én handling for at gemme",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Kunne ikke uploade vedhæftning, Prøv venligst igen", "UPLOAD_ERROR": "Kunne ikke uploade vedhæftning, Prøv venligst igen",
"LABEL_IDLE": "Upload Vedhæftning", "LABEL_IDLE": "Upload Vedhæftning",
"LABEL_UPLOADING": "Uploader...", "LABEL_UPLOADING": "Uploader...",
"LABEL_UPLOADED": "Succesfuldt Uploadet", "LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Mislykkedes" "LABEL_UPLOAD_FAILED": "Upload Mislykkedes"
} }
} }

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} samtaler valgt", "CONVERSATIONS_SELECTED": "%{conversationCount} samtaler valgt",
"AGENT_SELECT_LABEL": "Vælg Agent", "AGENT_SELECT_LABEL": "Vælg Agent",
"ASSIGN_CONFIRMATION_LABEL": "Er du sikker på, at du vil tildele %{conversationCount} %{conversationLabel} til", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Gå tilbage", "GO_BACK_LABEL": "Gå tilbage",
"ASSIGN_LABEL": "Tildel", "ASSIGN_LABEL": "Tildel",
"YES": "Ja",
"ASSIGN_AGENT_TOOLTIP": "Tildel Agent", "ASSIGN_AGENT_TOOLTIP": "Tildel Agent",
"ASSIGN_SUCCESFUL": "Samtaler tildelt", "ASSIGN_SUCCESFUL": "Samtaler tildelt",
"ASSIGN_FAILED": "Mislykkedes at tildele samtaler, prøv igen", "ASSIGN_FAILED": "Mislykkedes at tildele samtaler, prøv igen",

View file

@ -208,7 +208,8 @@
"CONVERSATION_LABELS": "Samtale Etiketter", "CONVERSATION_LABELS": "Samtale Etiketter",
"CONVERSATION_INFO": "Samtale Information", "CONVERSATION_INFO": "Samtale Information",
"CONTACT_ATTRIBUTES": "Kontakt Attributter", "CONTACT_ATTRIBUTES": "Kontakt Attributter",
"PREVIOUS_CONVERSATION": "Tidligere Samtaler" "PREVIOUS_CONVERSATION": "Tidligere Samtaler",
"MACROS": "Macros"
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

Some files were not shown because too many files have changed in this diff Show more