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/survey/i18n/index.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
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
# enable the following configuration
# 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
# 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=chatwoot.com

View file

@ -6,6 +6,7 @@ labels: 'Bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
@ -16,11 +17,11 @@ Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
4. See the error
**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**
@ -28,27 +29,50 @@ If applicable, add screenshots to help explain your problem.
**Browser logs**
Share the browser logs to debug the issue further
Share the browser logs to debug the issue further.
**Server logs**
Share the server logs to debug the issue further
Share the server logs to debug the issue further.
**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):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- [ ] app.chatwoot.com (Chatwoot Cloud)
- [ ] Self-hosted
- - [ ] 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]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
**Smartphone (please complete the following information)** (If applicable)
- Device: [e.g. iPhone6, Pixel7]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Browser [e.g. stock browser, firefox, safari]
- 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**
Add any other context about the problem here.

View file

@ -2,8 +2,7 @@
## 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)
## Type of change
@ -12,18 +11,18 @@ Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] 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
## 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:
- [ ] 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 made corresponding changes to the documentation
- [ ] My changes generate no new warnings

2
.gitignore vendored
View file

@ -60,3 +60,5 @@ test/cypress/videos/*
/config/master.key
/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)
newrelic_rpm (8.9.0)
nio4r (2.5.8)
nokogiri (1.13.7)
nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.7-arm64-darwin)
nokogiri (1.13.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.7-x86_64-darwin)
nokogiri (1.13.9-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.7-x86_64-linux)
nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
orm_adapter (0.5.0)
@ -808,4 +808,4 @@ RUBY VERSION
ruby 3.0.4p208
BUNDLED WITH
2.3.18
2.3.16

View file

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

View file

@ -35,7 +35,13 @@ class Messages::MessageBuilder
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

View file

@ -2,7 +2,8 @@ class Messages::Messenger::MessageBuilder
include ::FileTypeHelper
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.save!
@ -80,4 +81,10 @@ class Messages::Messenger::MessageBuilder
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{}
end
private
def unsupported_file_type?(attachment_type)
[:template, :unsupported_type].include? attachment_type.to_sym
end
end

View file

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

View file

@ -1,6 +1,6 @@
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
def index
@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?
@macro.save!
process_attachments
@macro
end
def show
@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok
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
ActiveRecord::Base.transaction do
@macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params)
process_attachments
@macro.save!
rescue StandardError => e
Rails.logger.error e
@ -42,6 +55,19 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok
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
params.permit(
:name, :account_id, :visibility,
@ -56,4 +82,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
def fetch_macro
@macro = Current.account.macros.find_by(id: params[:id])
end
def check_authorization
authorize(@macro) if @macro.present?
end
end

View file

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

View file

@ -50,7 +50,9 @@ class Api::V1::Widget::BaseController < ApplicationController
end
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
def contact_phone_number

View file

@ -17,7 +17,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
@contact = ContactIdentifyAction.new(
contact: @contact,
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
end

View file

@ -1,18 +1,17 @@
class Platform::Api::V1::AccountsController < PlatformController
def create
@resource = Account.new(account_params)
update_resource_features
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end
def show
render json: @resource
end
def show; end
def update
@resource.update!(account_params)
render json: @resource
@resource.assign_attributes(account_params)
update_resource_features
@resource.save!
end
def destroy
@ -27,6 +26,18 @@ class Platform::Api::V1::AccountsController < PlatformController
end
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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
class Public::Api::V1::Portals::ArticlesController < PublicController
before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal
before_action :set_category
before_action :set_category, except: [:index]
before_action :set_article, only: [:show]
layout 'portal'
@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
end
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
def portal

View file

@ -8,6 +8,12 @@ module FileTypeHelper
:file
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)
[
'image/jpeg',

View file

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

View file

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

View file

@ -105,6 +105,16 @@ class ConversationApi extends ApiClient {
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();

View file

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

View file

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

View file

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

View file

@ -1,17 +1,15 @@
<template>
<span class="time-ago">
<span> {{ timeAgo }}</span>
<span>{{ timeAgo }}</span>
</span>
</template>
<script>
const ZERO = 0;
const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
import timeMixin from 'dashboard/mixins/time';
import { differenceInMilliseconds } from 'date-fns';
export default {
name: 'TimeAgo',
@ -28,51 +26,40 @@ export default {
},
data() {
return {
timeAgo: '',
timeAgo: this.dynamicTime(this.timestamp),
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() {
const timeDiff = differenceInMilliseconds(
new Date(),
new Date(this.timestamp * 1000)
);
const timeDiff = Date.now() - this.timestamp * 1000;
if (timeDiff > DAY_IN_MILLI_SECONDS) {
return DAY_IN_MILLI_SECONDS;
}
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
return HOUR_IN_MILLI_SECONDS;
}
if (timeDiff > 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);
}
return MINUTE_IN_MILLI_SECONDS;
},
},
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,12 @@
:url="attachment.data_url"
: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
v-else
:url="attachment.data_url"
@ -119,6 +125,7 @@ import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File';
import BubbleVideo from './bubble/Video.vue';
import BubbleActions from './bubble/Actions';
import BubbleLocation from './bubble/Location';
import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
@ -136,6 +143,7 @@ export default {
BubbleFile,
BubbleVideo,
BubbleMailHead,
BubbleLocation,
ContextMenu,
Spinner,
},

View file

@ -139,7 +139,6 @@ export default {
listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
loadingChatList: 'getChatListLoadingStatus',
conversationLastSeen: 'getConversationLastSeen',
}),
inboxId() {
return this.currentChat.inbox_id;
@ -234,7 +233,6 @@ export default {
return 'arrow-chevron-left';
},
getLastSeenAt() {
if (this.conversationLastSeen) return this.conversationLastSeen;
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
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)"
/>
</menu-item-with-submenu>
<menu-item-with-submenu :option="labelMenuConfig">
<menu-item-with-submenu
:option="labelMenuConfig"
:sub-menu-available="!!labels.length"
>
<template>
<menu-item
v-for="label in labels"
@ -28,7 +31,10 @@
/>
</template>
</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" />
<template v-else>
<menu-item
@ -40,7 +46,10 @@
/>
</template>
</menu-item-with-submenu>
<menu-item-with-submenu :option="teamMenuConfig">
<menu-item-with-submenu
:option="teamMenuConfig"
:sub-menu-available="!!teams.length"
>
<menu-item
v-for="team in teams"
:key="team.id"

View file

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

View file

@ -1,11 +1,14 @@
<template>
<div class="menu-with-submenu flex-between">
<div
class="menu-with-submenu flex-between"
:class="{ disabled: !subMenuAvailable }"
>
<div class="menu-left">
<fluent-icon :icon="option.icon" size="14" class="menu-icon" />
<p class="menu-label">{{ option.label }}</p>
</div>
<fluent-icon icon="chevron-right" size="12" />
<div class="submenu">
<div v-if="subMenuAvailable" class="submenu">
<slot />
</div>
</div>
@ -18,6 +21,10 @@ export default {
type: Object,
default: () => {},
},
subMenuAvailable: {
type: Boolean,
default: true,
},
},
};
</script>
@ -55,6 +62,11 @@ export default {
left: 100%;
top: 0;
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 {
@ -73,5 +85,10 @@ export default {
clip-path: polygon(100% 0, 0% 0%, 100% 100%);
}
}
&.disabled {
opacity: 50%;
cursor: not-allowed;
}
}
</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,
'first.reply.created': this.onFirstReplyCreated,
'conversation.read': this.onConversationRead,
'conversation.updated': this.onConversationUpdated,
};
}
@ -67,8 +68,7 @@ class ActionCableConnector extends BaseActionCableConnector {
};
onConversationRead = data => {
const { contact_last_seen_at: lastSeen } = data;
this.app.$store.dispatch('updateConversationRead', lastSeen);
this.app.$store.dispatch('updateConversation', data);
};
onLogout = () => AuthAPI.logout();
@ -85,6 +85,11 @@ class ActionCableConnector extends BaseActionCableConnector {
this.fetchConversationStats();
};
onConversationUpdated = data => {
this.app.$store.dispatch('updateConversation', data);
this.fetchConversationStats();
};
onTypingOn = ({ conversation, user }) => {
const conversationId = conversation.id;

View file

@ -22,7 +22,7 @@ const generatePayload = data => {
let payload = actions.map(item => {
if (Array.isArray(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];
} else if (!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 => {
// Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data));
@ -23,8 +11,6 @@ const generatePayload = data => {
} else {
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;
});
// 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',
filter_operator: 'equal_to',
values: [
{ id: 'PENDING', name: 'Pending' },
{ id: 'pending', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' },
],
query_operator: 'and',
@ -18,7 +18,7 @@ const testData = [
account_id: 1,
auto_offline: true,
confirmed: true,
email: 'fayazara@gmail.com',
email: 'fayaz@test.com',
available_name: 'Fayaz',
name: 'Fayaz',
role: 'agent',
@ -52,7 +52,7 @@ const finalResult = {
{
attribute_key: 'id',
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": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
},
"CONDITION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
},
"ACTION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "تعذر تحميل المرفق، الرجاء المحاولة مرة أخرى",
"LABEL_IDLE": "ارفع المرفق",
"LABEL_UPLOADING": "جاري الرفع...",
"LABEL_UPLOADED": "تم الرفع بنجاح",
"LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "فشل الرفع"
}
}

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
"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": "العودة للخلف",
"ASSIGN_LABEL": "تكليف",
"YES": "نعم",
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",

View file

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

View file

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

View file

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

View file

@ -239,7 +239,9 @@
},
"API_CALLBACK": {
"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": "إنشاء قناة واتساب",
"API": {
@ -357,7 +359,7 @@
},
"FINISH": {
"TITLE": "أصبحت قناة التواصل جاهزة الآن!",
"MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة ",
"MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة",
"BUTTON_TEXT": "خذني إلى هناك",
"MORE_SETTINGS": "المزيد من الإعدادات",
"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": "تنزيل",
"UPLOADING": "جاري الرفع..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": {
"SUBMIT": "إرسال"
}
@ -179,6 +182,7 @@
"CONTACTS": "جهات الاتصال",
"HOME": "الرئيسية",
"AGENTS": "موظف الدعم",
"AGENT_BOTS": "Bots",
"INBOXES": "قنوات التواصل",
"NOTIFICATIONS": "الإشعارات",
"CANNED_RESPONSES": "الردود السريعة",
@ -189,6 +193,7 @@
"LABELS": "الوسوم",
"CUSTOM_ATTRIBUTES": "سمات مخصصة",
"AUTOMATION": "الأتمتة",
"MACROS": "Macros",
"TEAMS": "الفرق",
"BILLING": "الفواتير",
"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"
},
"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": {
"DELETE_MESSAGE": "You need to have atleast one action to save",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "Качване...",
"LABEL_UPLOADED": "Succesfully Uploaded",
"LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed"
}
}

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"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",
"ASSIGN_LABEL": "Assign",
"YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

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

View file

@ -54,7 +54,8 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one"
"SELECT_ONE": "Select one",
"SELECT": "Select"
}
},
"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": {
"LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.",
"ERROR": "Custom Domain is required"
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Enter a valid domain URL"
},
"HOME_PAGE_LINK": {
"LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.",
"ERROR": "Home Page Link is required"
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Enter a valid home page URL"
},
"THEME_COLOR": {
"LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": {
"API": {
"ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully"
"SUCCESS": "Article published successfully"
}
},
"ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
},
"API_CALLBACK": {
"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",
"API": {
@ -357,7 +359,7 @@
},
"FINISH": {
"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",
"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."

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"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",
"ASSIGN_LABEL": "Assignar",
"YES": "Si",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,7 +54,8 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "Presiona retorn (tecla enter) per seleccionar",
"ENTER_TO_REMOVE": "Presiona retorn (tecla enter) per eliminar",
"SELECT_ONE": "Selecciona un"
"SELECT_ONE": "Selecciona un",
"SELECT": "Select"
}
},
"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": {
"LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.",
"ERROR": "Custom Domain is required"
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Enter a valid domain URL"
},
"HOME_PAGE_LINK": {
"LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.",
"ERROR": "Home Page Link is required"
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Enter a valid home page URL"
},
"THEME_COLOR": {
"LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": {
"API": {
"ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully"
"SUCCESS": "Article published successfully"
}
},
"ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
},
"API_CALLBACK": {
"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",
"API": {
@ -357,7 +359,7 @@
},
"FINISH": {
"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à",
"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."

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",
"UPLOADING": "S'està carregant..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": {
"SUBMIT": "Envia"
}
@ -173,12 +176,13 @@
"SWITCH": "Switch",
"CONVERSATIONS": "Converses",
"ALL_CONVERSATIONS": "All Conversations",
"MENTIONED_CONVERSATIONS": "Mentions",
"MENTIONED_CONVERSATIONS": "Mencions",
"REPORTS": "Informes",
"SETTINGS": "Configuracions",
"CONTACTS": "Contactes",
"HOME": "Inici",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
"INBOXES": "Safates d'entrada",
"NOTIFICATIONS": "Notificacions",
"CANNED_RESPONSES": "Respostes predeterminades",
@ -189,14 +193,15 @@
"LABELS": "Etiquetes",
"CUSTOM_ATTRIBUTES": "Atributs personalitzats",
"AUTOMATION": "Automation",
"MACROS": "Macros",
"TEAMS": "Equips",
"BILLING": "Billing",
"CUSTOM_VIEWS_FOLDER": "Folders",
"CUSTOM_VIEWS_SEGMENTS": "Segments",
"ALL_CONTACTS": "All Contacts",
"TAGGED_WITH": "Tagged with",
"NEW_LABEL": "New label",
"NEW_TEAM": "New team",
"NEW_LABEL": "Nova etiqueta",
"NEW_TEAM": "Nou equip",
"NEW_INBOX": "New inbox",
"REPORTS_CONVERSATION": "Converses",
"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"
},
"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": {
"DELETE_MESSAGE": "You need to have atleast one action to save",
@ -109,7 +111,7 @@
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "Nahrávání...",
"LABEL_UPLOADED": "Succesfully Uploaded",
"LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed"
}
}

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"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",
"ASSIGN_LABEL": "Přiřadit",
"YES": "Ano",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",

View file

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

View file

@ -54,7 +54,8 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "Stiskněte Enter pro vybrání",
"ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
"SELECT_ONE": "Vyberte jeden"
"SELECT_ONE": "Vyberte jeden",
"SELECT": "Select"
}
},
"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": {
"LABEL": "Custom Domain",
"PLACEHOLDER": "Portal custom domain",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.",
"ERROR": "Custom Domain is required"
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Enter a valid domain URL"
},
"HOME_PAGE_LINK": {
"LABEL": "Home Page Link",
"PLACEHOLDER": "Portal home page link",
"HELP_TEXT": "The link used to return from the portal to the home page.",
"ERROR": "Home Page Link is required"
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Enter a valid home page URL"
},
"THEME_COLOR": {
"LABEL": "Portal theme color",
@ -306,7 +306,7 @@
"PUBLISH_ARTICLE": {
"API": {
"ERROR": "Error while publishing article",
"SUCCESS": "Article publishied successfully"
"SUCCESS": "Article published successfully"
}
},
"ARCHIVE_ARTICLE": {

View file

@ -239,7 +239,9 @@
},
"API_CALLBACK": {
"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",
"API": {
@ -357,7 +359,7 @@
},
"FINISH": {
"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",
"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ě."

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",
"UPLOADING": "Nahrávání..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": {
"SUBMIT": "Odeslat"
}
@ -179,6 +182,7 @@
"CONTACTS": "Kontakty",
"HOME": "Domů",
"AGENTS": "Agenti",
"AGENT_BOTS": "Bots",
"INBOXES": "Schránky",
"NOTIFICATIONS": "Oznámení",
"CANNED_RESPONSES": "Konzervované odpovědi",
@ -189,6 +193,7 @@
"LABELS": "Štítky",
"CUSTOM_ATTRIBUTES": "Vlastní atributy",
"AUTOMATION": "Automation",
"MACROS": "Macros",
"TEAMS": "Týmy",
"BILLING": "Billing",
"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"
},
"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": {
"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",
"LABEL_IDLE": "Upload Vedhæftning",
"LABEL_UPLOADING": "Uploader...",
"LABEL_UPLOADED": "Succesfuldt Uploadet",
"LABEL_UPLOADED": "Successfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Mislykkedes"
}
}

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} samtaler valgt",
"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",
"ASSIGN_LABEL": "Tildel",
"YES": "Ja",
"ASSIGN_AGENT_TOOLTIP": "Tildel Agent",
"ASSIGN_SUCCESFUL": "Samtaler tildelt",
"ASSIGN_FAILED": "Mislykkedes at tildele samtaler, prøv igen",

View file

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

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