Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ed5a4b4f | ||
|
|
7ecb993d06 | ||
|
|
c00a157c03 | ||
|
|
3750689bee | ||
|
|
0511bf4ba6 | ||
|
|
9f50638a40 | ||
|
|
f5ac272e3b | ||
|
|
2ba7318817 | ||
|
|
412303d1cd | ||
|
|
47b9399208 | ||
|
|
0830ad78b6 | ||
|
|
0b8df762f9 | ||
|
|
f24c87f9b2 | ||
|
|
f62a4170b6 | ||
|
|
2f090dbc61 | ||
|
|
ec17682d7a | ||
|
|
7861ce8c47 | ||
|
|
e5de41fea4 | ||
|
|
adb0e94cbd | ||
|
|
42fb083491 | ||
|
|
9101e1e221 | ||
|
|
91bac16c29 | ||
|
|
97699f3087 | ||
|
|
1242cfd704 | ||
|
|
6963c668bf | ||
|
|
a8c12ffb25 | ||
|
|
97d4ce65c4 | ||
|
|
ea787fecd8 | ||
|
|
ee02923ace | ||
|
|
0677d8763d | ||
|
|
095aaf3de6 | ||
|
|
b7aea99f44 | ||
|
|
d0bf66fe7a | ||
|
|
490fc4eaba | ||
|
|
e6e0f7de5a | ||
|
|
d2eda8bd08 | ||
|
|
55bf05d2d4 | ||
|
|
5b22af6af8 | ||
|
|
9338bc1391 | ||
|
|
88a16b8e96 |
+113
-133
@@ -1,84 +1,87 @@
|
||||
# Ruby CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
||||
#
|
||||
version: 2
|
||||
version: 2.1
|
||||
orbs:
|
||||
node: circleci/node@6.1.0
|
||||
|
||||
defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.3.3-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
- image: cimg/postgres:15.3
|
||||
- image: cimg/redis:6.2.6
|
||||
environment:
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
- COVERAGE: true
|
||||
- LOG_LEVEL: warn
|
||||
parallelism: 4
|
||||
machine:
|
||||
image: ubuntu-2204:2024.05.1
|
||||
resource_class: large
|
||||
environment:
|
||||
RAILS_LOG_TO_STDOUT: false
|
||||
COVERAGE: true
|
||||
LOG_LEVEL: warn
|
||||
parallelism: 4
|
||||
|
||||
jobs:
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
node-version: '20.12'
|
||||
- node/install-pnpm
|
||||
- node/install-packages:
|
||||
pkg-manager: pnpm
|
||||
override-ci-command: pnpm i
|
||||
- run: node --version
|
||||
- run: pnpm --version
|
||||
|
||||
- run:
|
||||
name: Configure Bundler
|
||||
name: Install System Dependencies
|
||||
command: |
|
||||
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
|
||||
source $BASH_ENV
|
||||
sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
|
||||
libpq-dev \
|
||||
redis-server \
|
||||
postgresql \
|
||||
build-essential \
|
||||
git \
|
||||
curl \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libreadline-dev \
|
||||
libyaml-dev \
|
||||
openjdk-11-jdk \
|
||||
jq \
|
||||
software-properties-common \
|
||||
ca-certificates \
|
||||
imagemagick \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
autoconf \
|
||||
gnupg2 \
|
||||
patch \
|
||||
ruby-dev \
|
||||
liblzma-dev \
|
||||
libgmp-dev \
|
||||
libncurses5-dev \
|
||||
libffi-dev \
|
||||
libgdbm6 \
|
||||
libgdbm-dev \
|
||||
libvips
|
||||
|
||||
- run:
|
||||
name: Install RVM and Ruby 3.3.3
|
||||
command: |
|
||||
sudo apt-get install -y gpg
|
||||
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||
\curl -sSL https://get.rvm.io | bash -s stable
|
||||
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||
source ~/.rvm/scripts/rvm
|
||||
rvm install "3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
gem install bundler
|
||||
|
||||
- run:
|
||||
name: Which bundler?
|
||||
command: bundle -v
|
||||
|
||||
- run:
|
||||
name: Swap node versions
|
||||
name: Install Application Dependencies
|
||||
command: |
|
||||
set +e
|
||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
nvm install v20
|
||||
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
|
||||
# Run bundler
|
||||
# Load installed gems from cache if possible, bundle install then save cache
|
||||
# Multiple caches are used to increase the chance of a cache hit
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
- run: bundle install --frozen --path ~/.bundle
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-
|
||||
|
||||
- run:
|
||||
name: yarn
|
||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
source ~/.rvm/scripts/rvm
|
||||
bundle install
|
||||
# pnpm install
|
||||
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
@@ -86,12 +89,8 @@ jobs:
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
|
||||
# verify swagger specification
|
||||
# Swagger verification
|
||||
- run:
|
||||
name: Verify swagger API specification
|
||||
command: |
|
||||
@@ -104,45 +103,62 @@ jobs:
|
||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||
|
||||
# Database setup
|
||||
- run: bundle exec rake db:create
|
||||
- run: bundle exec rake db:schema:load
|
||||
# we remove the FRONTED_URL from the .env before running the tests
|
||||
- run:
|
||||
name: Database Setup and Configure Environment Variables
|
||||
command: |
|
||||
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||
sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql
|
||||
chmod 644 ${PWD}/.circleci/setup_chatwoot.sql
|
||||
mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/
|
||||
sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql
|
||||
cp .env.example .env
|
||||
sed -i '/^FRONTEND_URL/d' .env
|
||||
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
|
||||
|
||||
# Database setup
|
||||
- run:
|
||||
name: Run DB migrations
|
||||
command: bundle exec rails db:chatwoot_prepare
|
||||
|
||||
# Bundle audit
|
||||
- run:
|
||||
name: Bundle audit
|
||||
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||
|
||||
# Rubocop linting
|
||||
- run:
|
||||
name: Rubocop
|
||||
command: bundle exec rubocop
|
||||
|
||||
# - run:
|
||||
# name: Brakeman
|
||||
# command: bundle exec brakeman
|
||||
|
||||
# ESLint linting
|
||||
- run:
|
||||
name: eslint
|
||||
command: yarn run eslint
|
||||
command: pnpm run eslint
|
||||
|
||||
# Run frontend tests
|
||||
- run:
|
||||
name: Run frontend tests
|
||||
command: |
|
||||
mkdir -p ~/tmp/test-results/frontend_specs
|
||||
mkdir -p ~/build/coverage/frontend
|
||||
~/tmp/cc-test-reporter before-build
|
||||
yarn test:coverage
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
pnpm run test:coverage
|
||||
|
||||
# Run rails tests
|
||||
- run:
|
||||
name: Code Climate Test Coverage (Frontend)
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
# Run backend tests
|
||||
- run:
|
||||
name: Run backend tests
|
||||
command: |
|
||||
mkdir -p ~/tmp/test-results/rspec
|
||||
mkdir -p ~/tmp/test-artifacts
|
||||
mkdir -p coverage
|
||||
mkdir -p ~/build/coverage/backend
|
||||
~/tmp/cc-test-reporter before-build
|
||||
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
bundle exec rspec --format progress \
|
||||
@@ -150,54 +166,18 @@ jobs:
|
||||
--out ~/tmp/test-results/rspec.xml \
|
||||
-- ${TESTFILES}
|
||||
no_output_timeout: 30m
|
||||
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
name: Code Climate Test Coverage (Backend)
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- run:
|
||||
name: List coverage directory contents
|
||||
command: |
|
||||
ls -R ~/build/coverage
|
||||
|
||||
- persist_to_workspace:
|
||||
root: coverage
|
||||
root: ~/build
|
||||
paths:
|
||||
- codeclimate.*.json
|
||||
# collect reports
|
||||
- store_test_results:
|
||||
path: ~/tmp/test-results
|
||||
- store_artifacts:
|
||||
path: ~/tmp/test-artifacts
|
||||
- store_artifacts:
|
||||
path: log
|
||||
|
||||
upload-coverage:
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:3.0.2-node-browsers
|
||||
environment:
|
||||
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/build
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
command: |
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
- run:
|
||||
name: Upload coverage results to Code Climate
|
||||
command: |
|
||||
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
commit:
|
||||
jobs:
|
||||
- build
|
||||
- upload-coverage:
|
||||
requires:
|
||||
- build
|
||||
- coverage
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE USER chatwoot CREATEDB;
|
||||
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
|
||||
ALTER ROLE chatwoot SUPERUSER;
|
||||
|
||||
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
|
||||
DROP DATABASE template1;
|
||||
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
|
||||
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
|
||||
|
||||
\c template1;
|
||||
VACUUM FREEZE;
|
||||
+2
-1
@@ -27,7 +27,8 @@ checks:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- 'spec/'
|
||||
- '**/specs/'
|
||||
- '**/specs/**/**'
|
||||
- '**/spec/**/**'
|
||||
- 'db/*'
|
||||
- 'bin/**/*'
|
||||
- 'db/**/*'
|
||||
|
||||
+24
-1
@@ -1,6 +1,23 @@
|
||||
module.exports = {
|
||||
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/vue3-recommended'],
|
||||
extends: [
|
||||
'airbnb-base/legacy',
|
||||
'prettier',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vitest-globals/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.spec.{j,t}s?(x)'],
|
||||
env: {
|
||||
'vitest-globals/env': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['html', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': ['error'],
|
||||
camelcase: 'off',
|
||||
@@ -206,5 +223,11 @@ module.exports = {
|
||||
globals: {
|
||||
bus: true,
|
||||
vi: true,
|
||||
// beforeEach: true,
|
||||
// afterEach: true,
|
||||
// test: true,
|
||||
// describe: true,
|
||||
// it: true,
|
||||
// expect: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Frontend Lint & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.3.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run eslint
|
||||
run: pnpm run eslint
|
||||
|
||||
- name: Run frontend tests with coverage
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
pnpm run test:coverage
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
image: postgres:15.3
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_PASSWORD: ''
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
@@ -41,46 +41,49 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm i
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
- name: Run frontend tests
|
||||
run: pnpm run test:coverage
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
|
||||
@@ -52,8 +52,11 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
accountUIFlags: 'accounts/getUIFlags',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
// currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
currentAccountId() {
|
||||
return this.$route.params.accountId;
|
||||
},
|
||||
hasAccounts() {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
@@ -70,15 +73,22 @@ export default {
|
||||
}
|
||||
},
|
||||
currentAccountId() {
|
||||
console.log('THIS CALL', this.currentAccountId);
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
} else {
|
||||
console.log('NO ACCOUNT ID');
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
created() {
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
},
|
||||
async mounted() {
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
this.initializeColorTheme();
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
await this.$store.dispatch('accounts/get');
|
||||
},
|
||||
unmounted() {
|
||||
if (this.reconnectService) {
|
||||
@@ -94,7 +104,7 @@ export default {
|
||||
mql.onchange = e => setColorTheme(e.matches);
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
this.$i18n.locale = locale;
|
||||
},
|
||||
async initializeAccount() {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
|
||||
@@ -27,14 +27,14 @@ export default {
|
||||
<div class="flex flex-col items-start px-8 pt-8 pb-0">
|
||||
<img v-if="headerImage" :src="headerImage" alt="No image" />
|
||||
<h2
|
||||
ref="modalHeaderTitle"
|
||||
data-test-id="modal-header-title"
|
||||
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="headerContent"
|
||||
ref="modalHeaderContent"
|
||||
data-test-id="modal-header-content"
|
||||
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ headerContent }}
|
||||
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
size="small"
|
||||
:color-scheme="status.disabled ? '' : 'secondary'"
|
||||
:variant="status.disabled ? 'smooth' : 'clear'"
|
||||
class-names="status-change--dropdown-button"
|
||||
class="status-change--dropdown-button"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
>
|
||||
<AvailabilityStatusBadge :status="status.value" />
|
||||
|
||||
@@ -120,6 +120,7 @@ export default {
|
||||
this.currentUser,
|
||||
this.accountId
|
||||
);
|
||||
|
||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||
return menuItems.filter(menuItem => {
|
||||
const isAvailableForTheUser = hasPermissions(
|
||||
@@ -130,6 +131,7 @@ export default {
|
||||
if (!isAvailableForTheUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||
!this.isACustomBrandedInstance
|
||||
@@ -137,10 +139,12 @@ export default {
|
||||
return true;
|
||||
}
|
||||
if (menuItem.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
const isEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
menuItem.featureFlag
|
||||
);
|
||||
|
||||
return isEnabled;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -50,6 +50,9 @@ export default {
|
||||
showOptionsMenu: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log('MENU ITEMS', this.menuItems);
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
toggleOptions() {
|
||||
|
||||
@@ -26,6 +26,13 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log(
|
||||
'MOUNTED: PRIMARY NAV ITEM',
|
||||
this.name,
|
||||
this.$t(`SIDEBAR.${this.name}`)
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
+42
-59
@@ -1,79 +1,62 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AccountSelector from '../AccountSelector.vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
import WootModal from 'dashboard/components/Modal.vue';
|
||||
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.component('woot-modal', WootModal);
|
||||
localVue.component('woot-modal-header', WootModalHeader);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
messages: i18n,
|
||||
const store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => 1,
|
||||
getCurrentUser: () => ({
|
||||
accounts: [
|
||||
{ id: 1, name: 'Chatwoot', role: 'administrator' },
|
||||
{ id: 2, name: 'GitX', role: 'agent' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
globalConfig: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
get: () => ({ createNewAccountFromDashboard: false }),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('accountSelctor', () => {
|
||||
describe('AccountSelector', () => {
|
||||
let accountSelector = null;
|
||||
const currentUser = {
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Chatwoot',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'GitX',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {};
|
||||
modules = {
|
||||
auth: {
|
||||
getters: {
|
||||
getCurrentAccountId: () => 1,
|
||||
getCurrentUser: () => currentUser,
|
||||
},
|
||||
},
|
||||
globalConfig: {
|
||||
getters: {
|
||||
'globalConfig/get': () => ({ createNewAccountFromDashboard: false }),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let store = new Vuex.Store({ actions, modules });
|
||||
accountSelector = mount(AccountSelector, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
propsData: { showAccountModal: true },
|
||||
stubs: { WootButton: { template: '<button />' } },
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
'woot-modal': WootModal,
|
||||
'woot-modal-header': WootModalHeader,
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
stubs: {
|
||||
WootButton: { template: '<button />' },
|
||||
// override global stub
|
||||
WootModalHeader: false,
|
||||
},
|
||||
},
|
||||
props: { showAccountModal: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('title and sub title exist', () => {
|
||||
const headerComponent = accountSelector.findComponent(WootModalHeader);
|
||||
const title = headerComponent.findComponent({ ref: 'modalHeaderTitle' });
|
||||
const title = headerComponent.find('[data-test-id="modal-header-title"]');
|
||||
expect(title.text()).toBe('Switch Account');
|
||||
const content = headerComponent.findComponent({
|
||||
ref: 'modalHeaderContent',
|
||||
});
|
||||
const content = headerComponent.find(
|
||||
'[data-test-id="modal-header-content"]'
|
||||
);
|
||||
expect(content.text()).toBe('Select an account from the following list');
|
||||
});
|
||||
|
||||
|
||||
+32
-40
@@ -1,27 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AgentDetails from '../AgentDetails.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('thumbnail', Thumbnail);
|
||||
localVue.component('woot-button', WootButton);
|
||||
localVue.component('woot-button', WootButton);
|
||||
localVue.use(VTooltip, {
|
||||
defaultHtml: false,
|
||||
});
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
messages: i18n,
|
||||
});
|
||||
|
||||
describe('agentDetails', () => {
|
||||
describe('AgentDetails', () => {
|
||||
const currentUser = {
|
||||
name: 'Neymar Junior',
|
||||
avatar_url: '',
|
||||
@@ -29,37 +12,46 @@ describe('agentDetails', () => {
|
||||
};
|
||||
const currentRole = 'agent';
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
let agentDetails = null;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {};
|
||||
const mockTooltipDirective = {
|
||||
mounted: (el, binding) => {
|
||||
// You can mock the behavior here if necessary
|
||||
el.setAttribute('data-tooltip', binding.value || '');
|
||||
},
|
||||
};
|
||||
|
||||
modules = {
|
||||
auth: {
|
||||
getters: {
|
||||
getCurrentUser: () => currentUser,
|
||||
getCurrentRole: () => currentRole,
|
||||
getCurrentUserAvailability: () => currentUser.availability_status,
|
||||
beforeEach(() => {
|
||||
store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentUser: () => currentUser,
|
||||
getCurrentRole: () => currentRole,
|
||||
getCurrentUserAvailability: () => currentUser.availability_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
actions,
|
||||
modules,
|
||||
});
|
||||
|
||||
agentDetails = shallowMount(AgentDetails, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
Thumbnail,
|
||||
WootButton,
|
||||
},
|
||||
directives: {
|
||||
tooltip: mockTooltipDirective, // Mocking the tooltip directive
|
||||
},
|
||||
stubs: { WootButton: { template: '<button><slot /></button>' } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(' the agent status', () => {
|
||||
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
|
||||
it('shows the correct agent status', () => {
|
||||
expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
|
||||
});
|
||||
|
||||
it('agent thumbnail exists', () => {
|
||||
|
||||
+32
-34
@@ -1,20 +1,7 @@
|
||||
import NotificationBell from '../NotificationBell.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
messages: i18n,
|
||||
});
|
||||
import NotificationBell from '../NotificationBell.vue';
|
||||
|
||||
const $route = {
|
||||
name: 'notifications_index',
|
||||
@@ -33,43 +20,51 @@ describe('notificationBell', () => {
|
||||
};
|
||||
modules = {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => accountId,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
'notifications/getMeta': () => notificationMetadata,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
store = createStore({
|
||||
actions,
|
||||
modules,
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return unread count 19 ', () => {
|
||||
it('it should return unread count 19', () => {
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
mocks: {
|
||||
$route,
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unreadCount).toBe('19');
|
||||
});
|
||||
|
||||
it('it should return unread count 99+ ', async () => {
|
||||
it('it should return unread count 99+', async () => {
|
||||
notificationMetadata.unreadCount = 100;
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
mocks: {
|
||||
$route,
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unreadCount).toBe('99+');
|
||||
@@ -77,11 +72,14 @@ describe('notificationBell', () => {
|
||||
|
||||
it('isNotificationPanelActive', async () => {
|
||||
const notificationBell = shallowMount(NotificationBell, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
mocks: {
|
||||
$route,
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AvailabilityStatus from '../AvailabilityStatus.vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import FloatingVue from 'floating-vue';
|
||||
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -11,70 +8,64 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(FloatingVue, {
|
||||
html: false,
|
||||
});
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('woot-button', WootButton);
|
||||
localVue.component('woot-dropdown-header', WootDropdownHeader);
|
||||
localVue.component('woot-dropdown-menu', WootDropdownMenu);
|
||||
localVue.component('woot-dropdown-divider', WootDropdownDivider);
|
||||
localVue.component('woot-dropdown-item', WootDropdownItem);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
|
||||
|
||||
describe('AvailabilityStatus', () => {
|
||||
const currentAvailability = 'online';
|
||||
const currentAccountId = '1';
|
||||
const currentUserAutoOffline = false;
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
let availabilityStatus = null;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
updateAvailability: vi.fn(() => {
|
||||
return Promise.resolve();
|
||||
}),
|
||||
updateAvailability: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
modules = {
|
||||
auth: {
|
||||
getters: {
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
||||
store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store = new Vuex.Store({ actions, modules });
|
||||
|
||||
availabilityStatus = mount(AvailabilityStatus, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
stubs: { WootSwitch: { template: '<button />' } },
|
||||
actions,
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches an action when user changes status', async () => {
|
||||
await availabilityStatus;
|
||||
availabilityStatus
|
||||
.findAll('.status-change--dropdown-button')
|
||||
.at(2)
|
||||
.trigger('click');
|
||||
const wrapper = mount(AvailabilityStatus, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
WootButton,
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownHeader,
|
||||
WootDropdownDivider,
|
||||
FluentIcon,
|
||||
},
|
||||
stubs: {
|
||||
WootSwitch: { template: '<button />' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(actions.updateAvailability).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
{ availability: 'offline', account_id: currentAccountId },
|
||||
undefined
|
||||
);
|
||||
// Ensure that the dropdown menu is opened
|
||||
await wrapper.vm.openStatusMenu();
|
||||
|
||||
// Simulate the user clicking the 3rd button (offline status)
|
||||
const buttons = wrapper.findAll('.status-change--dropdown-button');
|
||||
expect(buttons.length).toBeGreaterThan(0); // Ensure buttons exist
|
||||
|
||||
await buttons[2].trigger('click');
|
||||
|
||||
expect(actions.updateAvailability).toHaveBeenCalledTimes(1);
|
||||
expect(actions.updateAvailability.mock.calls[0][1]).toEqual({
|
||||
availability: 'offline',
|
||||
account_id: currentAccountId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,5 +7,8 @@ exports[`SidemenuIcon > matches snapshot 1`] = `
|
||||
icon="list"
|
||||
size="small"
|
||||
variant="clear"
|
||||
/>
|
||||
>
|
||||
|
||||
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -84,10 +84,10 @@ const shouldShowEmptyState = computed(() => {
|
||||
<slot name="search">
|
||||
<DropdownSearch
|
||||
v-if="enableSearch"
|
||||
:input-value="searchTerm"
|
||||
v-model="searchTerm"
|
||||
:input-placeholder="inputPlaceholder"
|
||||
:show-clear-filter="showClearFilter"
|
||||
@input="onSearch"
|
||||
@update:model-value="onSearch"
|
||||
@remove="$emit('removeFilter')"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
import { defineEmits, defineModel } from 'vue';
|
||||
defineProps({
|
||||
inputValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -15,7 +11,12 @@ defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['input', 'remove']);
|
||||
const emit = defineEmits(['remove']);
|
||||
|
||||
const value = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,16 +30,15 @@ const emit = defineEmits(['input', 'remove']);
|
||||
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
|
||||
/>
|
||||
<input
|
||||
v-model="value"
|
||||
:placeholder="inputPlaceholder"
|
||||
type="text"
|
||||
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
|
||||
:placeholder="inputPlaceholder"
|
||||
:value="inputValue"
|
||||
@input="emit('input', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Clear filter button -->
|
||||
<woot-button
|
||||
v-if="!inputValue && showClearFilter"
|
||||
v-if="!modelValue && showClearFilter"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script>
|
||||
export default {
|
||||
// The value types are dynamic, hence prop validation removed to work with our action schema
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['teams', 'value'],
|
||||
emits: ['input'],
|
||||
props: {
|
||||
teams: { type: Array, required: true },
|
||||
modelValue: { type: Object, required: true },
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
selectedTeams: [],
|
||||
@@ -11,13 +12,13 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const { team_ids: teamIds } = this.value;
|
||||
const { team_ids: teamIds } = this.modelValue;
|
||||
this.selectedTeams = teamIds;
|
||||
this.message = this.value.message;
|
||||
this.message = this.modelValue.message;
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
this.$emit('input', {
|
||||
this.$emit('update:modelValue', {
|
||||
team_ids: this.selectedTeams.map(team => team.id),
|
||||
message: this.message,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,12 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['recorderProgressChanged', 'finishRecord']);
|
||||
const emit = defineEmits([
|
||||
'recorderProgressChanged',
|
||||
'finishRecord',
|
||||
'pause',
|
||||
'play',
|
||||
]);
|
||||
|
||||
const waveformContainer = ref(null);
|
||||
const wavesurfer = ref(null);
|
||||
@@ -47,6 +52,9 @@ const initWaveSurfer = () => {
|
||||
],
|
||||
});
|
||||
|
||||
wavesurfer.value.on('pause', () => emit('pause'));
|
||||
wavesurfer.value.on('play', () => emit('play'));
|
||||
|
||||
record.value = wavesurfer.value.plugins[0];
|
||||
|
||||
wavesurfer.value.on('finish', () => {
|
||||
@@ -106,11 +114,9 @@ onUnmounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ playPause, stopRecording });
|
||||
defineExpose({ playPause, stopRecording, record });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div ref="waveformContainer" />
|
||||
</div>
|
||||
<div ref="waveformContainer" class="w-full p-1" />
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
@@ -34,7 +35,7 @@ export default {
|
||||
},
|
||||
recordingAudioDurationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: '00:00',
|
||||
},
|
||||
// inbox prop is used in /mixins/inboxMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
@@ -122,6 +123,8 @@ export default {
|
||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||
useUISettings();
|
||||
|
||||
const uploadRef = ref(false);
|
||||
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyA': {
|
||||
action: () => {
|
||||
@@ -143,6 +146,7 @@ export default {
|
||||
return {
|
||||
setSignatureFlagForInbox,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
uploadRef,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -255,13 +259,13 @@ export default {
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
icon="emoji"
|
||||
emoji="😊"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
<FileUpload
|
||||
ref="uploadRef"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
input-id="conversationAttachment"
|
||||
:size="4096 * 4096"
|
||||
@@ -280,7 +284,6 @@ export default {
|
||||
class-names="button--upload"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
icon="attach"
|
||||
emoji="📎"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -290,7 +293,6 @@ export default {
|
||||
v-if="showAudioRecorderButton"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||
emoji="🎤"
|
||||
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -300,7 +302,6 @@ export default {
|
||||
v-if="showEditorToggle"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
icon="quote"
|
||||
emoji="🖊️"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -309,7 +310,6 @@ export default {
|
||||
<woot-button
|
||||
v-if="showAudioPlayStopButton"
|
||||
:icon="audioRecorderPlayStopIcon"
|
||||
emoji="🎤"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -350,7 +350,7 @@ export default {
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.uploadRef && $refs.uploadRef.dropActive"
|
||||
v-show="uploadRef && uploadRef.dropActive"
|
||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import lamejs from '@breezystack/lamejs';
|
||||
|
||||
const writeString = (view, offset, string) => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
@@ -28,7 +29,9 @@ const bufferToWav = async (buffer, numChannels, sampleRate) => {
|
||||
|
||||
// WAV Data
|
||||
const offset = 44;
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let channel = 0; channel < numChannels; channel++) {
|
||||
const sample = Math.max(
|
||||
-1,
|
||||
|
||||
@@ -478,6 +478,7 @@ export default {
|
||||
</div>
|
||||
<ul class="conversation-panel">
|
||||
<transition name="slide-up">
|
||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||
<li class="min-h-[4rem]">
|
||||
<span v-if="shouldShowSpinner" class="spinner message" />
|
||||
</li>
|
||||
|
||||
@@ -19,7 +19,7 @@ import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
@@ -50,20 +50,20 @@ const EmojiInput = defineAsyncComponent(
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiInput,
|
||||
CannedResponse,
|
||||
ReplyToMessage,
|
||||
ResizableTextArea,
|
||||
AttachmentPreview,
|
||||
ReplyTopPanel,
|
||||
ReplyEmailHead,
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
WootAudioRecorder,
|
||||
Banner,
|
||||
WhatsappTemplates,
|
||||
MessageSignatureMissingAlert,
|
||||
ArticleSearchPopover,
|
||||
AttachmentPreview,
|
||||
AudioRecorder,
|
||||
Banner,
|
||||
CannedResponse,
|
||||
EmojiInput,
|
||||
MessageSignatureMissingAlert,
|
||||
ReplyBottomPanel,
|
||||
ReplyEmailHead,
|
||||
ReplyToMessage,
|
||||
ReplyTopPanel,
|
||||
ResizableTextArea,
|
||||
WhatsappTemplates,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
emits: ['update:popoutReplyBox', 'togglePopout'],
|
||||
@@ -115,6 +115,7 @@ export default {
|
||||
showVariablesMenu: false,
|
||||
newConversationModalActive: false,
|
||||
showArticleSearchPopover: false,
|
||||
hasRecordedAudio: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -279,12 +280,6 @@ export default {
|
||||
hasAttachments() {
|
||||
return this.attachedFiles.length;
|
||||
},
|
||||
hasRecordedAudio() {
|
||||
return (
|
||||
this.$refs.audioRecorderInput &&
|
||||
this.$refs.audioRecorderInput.hasAudio()
|
||||
);
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||
},
|
||||
@@ -364,7 +359,7 @@ export default {
|
||||
return AUDIO_FORMATS.MP3;
|
||||
}
|
||||
if (this.isAPIInbox) {
|
||||
return AUDIO_FORMATS.OGG;
|
||||
return AUDIO_FORMATS.MP3;
|
||||
}
|
||||
return AUDIO_FORMATS.WAV;
|
||||
},
|
||||
@@ -803,19 +798,14 @@ export default {
|
||||
this.attachedFiles = [];
|
||||
this.isRecordingAudio = false;
|
||||
this.resetReplyToMessage();
|
||||
this.resetAudioRecorderInput();
|
||||
},
|
||||
clearEmailField() {
|
||||
this.ccEmails = '';
|
||||
this.bccEmails = '';
|
||||
this.toEmails = '';
|
||||
},
|
||||
clearRecorder() {
|
||||
this.isRecordingAudio = false;
|
||||
// Only clear the recorded audio when we click toggle button.
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
file => !file?.isRecordedAudio
|
||||
);
|
||||
},
|
||||
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
@@ -823,17 +813,18 @@ export default {
|
||||
this.isRecordingAudio = !this.isRecordingAudio;
|
||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||
if (!this.isRecordingAudio) {
|
||||
this.clearRecorder();
|
||||
this.resetAudioRecorderInput();
|
||||
}
|
||||
},
|
||||
toggleAudioRecorderPlayPause() {
|
||||
if (this.isRecordingAudio) {
|
||||
if (!this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopRecording();
|
||||
} else if (this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
if (!this.isRecordingAudio) {
|
||||
return;
|
||||
}
|
||||
if (!this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopRecording();
|
||||
} else if (this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
@@ -860,13 +851,9 @@ export default {
|
||||
onRecordProgressChanged(duration) {
|
||||
this.recordingAudioDurationText = duration;
|
||||
},
|
||||
onStateRecorderChanged(state) {
|
||||
this.recordingAudioState = state;
|
||||
if (state && 'notallowederror'.includes(state)) {
|
||||
this.toggleAudioRecorder();
|
||||
}
|
||||
},
|
||||
onFinishRecorder(file) {
|
||||
this.recordingAudioState = 'stopped';
|
||||
this.hasRecordedAudio = true;
|
||||
// Added a new key isRecordedAudio to the file to find it's and recorded audio
|
||||
// Because to filter and show only non recorded audio and other attachments
|
||||
const autoRecordedFile = {
|
||||
@@ -1067,6 +1054,16 @@ export default {
|
||||
toggleInsertArticle() {
|
||||
this.showArticleSearchPopover = !this.showArticleSearchPopover;
|
||||
},
|
||||
resetAudioRecorderInput() {
|
||||
this.recordingAudioDurationText = '00:00';
|
||||
this.isRecordingAudio = false;
|
||||
this.recordingAudioState = '';
|
||||
this.hasRecordedAudio = false;
|
||||
// Only clear the recorded audio when we click toggle button.
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
file => !file?.isRecordedAudio
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1122,13 +1119,14 @@ export default {
|
||||
v-model:bcc-emails="bccEmails"
|
||||
v-model:to-emails="toEmails"
|
||||
/>
|
||||
<WootAudioRecorder
|
||||
<AudioRecorder
|
||||
v-if="showAudioRecorderEditor"
|
||||
ref="audioRecorderInput"
|
||||
:audio-record-format="audioRecordFormat"
|
||||
@recorder-progress-changed="onRecordProgressChanged"
|
||||
@state-recorder-changed="onStateRecorderChanged"
|
||||
@finish-record="onFinishRecorder"
|
||||
@play="recordingAudioState = 'playing'"
|
||||
@pause="recordingAudioState = 'paused'"
|
||||
/>
|
||||
<ResizableTextArea
|
||||
v-else-if="!showRichContentEditor"
|
||||
@@ -1169,7 +1167,11 @@ export default {
|
||||
@clear-selection="clearEditorSelection"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
||||
<div
|
||||
v-if="hasAttachments && !showAudioRecorderEditor"
|
||||
class="attachment-preview-box"
|
||||
@paste="onPaste"
|
||||
>
|
||||
<AttachmentPreview
|
||||
class="flex-col mt-4"
|
||||
:attachments="attachedFiles"
|
||||
|
||||
+49
-59
@@ -1,11 +1,7 @@
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import FloatingVue from 'floating-vue';
|
||||
import Button from 'dashboard/components/buttons/Button.vue';
|
||||
import i18n from 'dashboard/i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import MoreActions from '../MoreActions.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
@@ -15,75 +11,67 @@ vi.mock('shared/helpers/mitt', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.use(FloatingVue);
|
||||
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
localVue.component('woot-button', Button);
|
||||
|
||||
localVue.prototype.$emitter = {
|
||||
emit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
const mockDirective = {
|
||||
mounted: () => {},
|
||||
};
|
||||
|
||||
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
describe('MoveActions', () => {
|
||||
let currentChat = { id: 8, muted: false };
|
||||
let state = null;
|
||||
let store = null;
|
||||
let muteConversation = null;
|
||||
let unmuteConversation = null;
|
||||
let modules = null;
|
||||
let getters = null;
|
||||
let store = null;
|
||||
let moreActions = null;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
authenticated: true,
|
||||
currentChat,
|
||||
};
|
||||
|
||||
muteConversation = vi.fn(() => Promise.resolve());
|
||||
unmuteConversation = vi.fn(() => Promise.resolve());
|
||||
|
||||
modules = {
|
||||
conversations: { actions: { muteConversation, unmuteConversation } },
|
||||
};
|
||||
|
||||
getters = { getSelectedChat: () => currentChat };
|
||||
|
||||
store = new Vuex.Store({ state, modules, getters });
|
||||
|
||||
moreActions = mount(MoreActions, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
stubs: {
|
||||
WootModal: { template: '<div><slot/> </div>' },
|
||||
WootModalHeader: { template: '<div><slot/> </div>' },
|
||||
store = createStore({
|
||||
state: {
|
||||
authenticated: true,
|
||||
currentChat,
|
||||
},
|
||||
getters: {
|
||||
getSelectedChat: () => currentChat,
|
||||
},
|
||||
modules: {
|
||||
conversations: {
|
||||
namespaced: false,
|
||||
actions: { muteConversation, unmuteConversation },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const createWrapper = () =>
|
||||
mount(MoreActions, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
directives: {
|
||||
'on-clickaway': mockDirective,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('muting discussion', () => {
|
||||
it('triggers "muteConversation"', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
const wrapper = createWrapper();
|
||||
await wrapper.find('button:first-child').trigger('click');
|
||||
|
||||
expect(muteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
currentChat.id,
|
||||
undefined
|
||||
expect(muteConversation).toHaveBeenCalledTimes(1);
|
||||
expect(muteConversation).toHaveBeenCalledWith(
|
||||
expect.any(Object), // First argument is the Vuex context object
|
||||
currentChat.id // Second argument is the ID of the conversation
|
||||
);
|
||||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
const wrapper = createWrapper();
|
||||
await wrapper.find('button:first-child').trigger('click');
|
||||
|
||||
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||
message:
|
||||
@@ -99,17 +87,19 @@ describe('MoveActions', () => {
|
||||
});
|
||||
|
||||
it('triggers "unmuteConversation"', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
const wrapper = createWrapper();
|
||||
await wrapper.find('button:first-child').trigger('click');
|
||||
|
||||
expect(unmuteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
currentChat.id,
|
||||
undefined
|
||||
expect(unmuteConversation).toHaveBeenCalledTimes(1);
|
||||
expect(unmuteConversation).toHaveBeenCalledWith(
|
||||
expect.any(Object), // First argument is the Vuex context object
|
||||
currentChat.id // Second argument is the ID of the conversation
|
||||
);
|
||||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
const wrapper = createWrapper();
|
||||
await wrapper.find('button:first-child').trigger('click');
|
||||
|
||||
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||
message: 'This contact is unblocked successfully.',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import analyticsHelper from '/dashboard/helper/AnalyticsHelper/index';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
|
||||
|
||||
/**
|
||||
* Custom hook to track events
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useEmitter } from '../emitter';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
@@ -10,31 +11,34 @@ vi.mock('shared/helpers/mitt', () => ({
|
||||
}));
|
||||
|
||||
describe('useEmitter', () => {
|
||||
let wrapper;
|
||||
const eventName = 'my-event';
|
||||
const callback = vi.fn();
|
||||
|
||||
let wrapper;
|
||||
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
cleanup: useEmitter(eventName, callback),
|
||||
};
|
||||
},
|
||||
template: '<div>Hello world</div>',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount({
|
||||
template: `
|
||||
<div>
|
||||
Hello world
|
||||
</div>
|
||||
`,
|
||||
setup() {
|
||||
return {
|
||||
cleanup: useEmitter(eventName, callback),
|
||||
};
|
||||
},
|
||||
});
|
||||
wrapper = shallowMount(TestComponent);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should add an event listener on mount', () => {
|
||||
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
|
||||
it('should remove the event listener when the component is unmounted', () => {
|
||||
wrapper.destroy();
|
||||
it('should remove the event listener when the component is unmounted', async () => {
|
||||
await wrapper.unmount();
|
||||
expect(emitter.off).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper';
|
||||
import { useTrack, useAlert } from '../index';
|
||||
|
||||
vi.mock('vue', () => ({
|
||||
getCurrentInstance: vi.fn(),
|
||||
}));
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
emit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
actual.default = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
return actual;
|
||||
});
|
||||
|
||||
describe('useTrack', () => {
|
||||
it('should return a function', () => {
|
||||
const track = useTrack();
|
||||
expect(typeof track).toBe('function');
|
||||
it('should call analyticsHelper.track and return a function', () => {
|
||||
const eventArgs = ['event-name', { some: 'data' }];
|
||||
useTrack(...eventArgs);
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith(...eventArgs);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,14 +4,20 @@ import {
|
||||
useStoreGetters,
|
||||
useMapGetter,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables');
|
||||
vi.mock('vue-i18n');
|
||||
vi.mock('dashboard/api/integrations/openapi');
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
actual.default = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
return actual;
|
||||
});
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
|
||||
OPEN_AI_EVENTS: {
|
||||
TEST_EVENT: 'open_ai_test_event',
|
||||
@@ -40,9 +46,7 @@ describe('useAI', () => {
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
useTrack.mockReturnValue(vi.fn());
|
||||
useI18n.mockReturnValue({ t: vi.fn() });
|
||||
useAlert.mockReturnValue(vi.fn());
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', async () => {
|
||||
@@ -78,13 +82,12 @@ describe('useAI', () => {
|
||||
});
|
||||
|
||||
it('records analytics correctly', async () => {
|
||||
const mockTrack = vi.fn();
|
||||
useTrack.mockReturnValue(mockTrack);
|
||||
// const mockTrack = analyticsHelper.track;
|
||||
const { recordAnalytics } = useAI();
|
||||
|
||||
await recordAnalytics('TEST_EVENT', { data: 'test' });
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
type: 'TEST_EVENT',
|
||||
data: 'test',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAutomation } from '../useAutomation';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from '../useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as automationHelper from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
customAttributes,
|
||||
@@ -20,7 +20,7 @@ import { MESSAGE_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables');
|
||||
vi.mock('../useI18n');
|
||||
vi.mock('vue-i18n');
|
||||
vi.mock('dashboard/helper/automationHelper');
|
||||
|
||||
describe('useAutomation', () => {
|
||||
@@ -120,8 +120,8 @@ describe('useAutomation', () => {
|
||||
});
|
||||
|
||||
it('appends new condition and action correctly', () => {
|
||||
const { appendNewCondition, appendNewAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
const { appendNewCondition, appendNewAction, automation } = useAutomation();
|
||||
automation.value = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
@@ -130,36 +130,37 @@ describe('useAutomation', () => {
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
appendNewCondition(mockAutomation);
|
||||
appendNewAction(mockAutomation);
|
||||
appendNewCondition();
|
||||
appendNewAction();
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
expect(automation.value.conditions).toHaveLength(1);
|
||||
expect(automation.value.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('removes filter and action correctly', () => {
|
||||
const { removeFilter, removeAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
const { removeFilter, removeAction, automation } = useAutomation();
|
||||
automation.value = {
|
||||
conditions: [{ id: 1 }, { id: 2 }],
|
||||
actions: [{ id: 1 }, { id: 2 }],
|
||||
};
|
||||
|
||||
removeFilter(mockAutomation, 0);
|
||||
removeAction(mockAutomation, 0);
|
||||
removeFilter(0);
|
||||
removeAction(0);
|
||||
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
expect(mockAutomation.conditions[0].id).toBe(2);
|
||||
expect(mockAutomation.actions[0].id).toBe(2);
|
||||
expect(automation.value.conditions).toHaveLength(1);
|
||||
expect(automation.value.actions).toHaveLength(1);
|
||||
expect(automation.value.conditions[0].id).toBe(2);
|
||||
expect(automation.value.actions[0].id).toBe(2);
|
||||
});
|
||||
|
||||
it('resets filter and action correctly', () => {
|
||||
const { resetFilter, resetAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
const { resetFilter, resetAction, automation, automationTypes } =
|
||||
useAutomation();
|
||||
automation.value = {
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
@@ -170,77 +171,37 @@ describe('useAutomation', () => {
|
||||
],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {
|
||||
message_created: {
|
||||
conditions: [
|
||||
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
|
||||
],
|
||||
},
|
||||
automationTypes.message_created = {
|
||||
conditions: [
|
||||
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
|
||||
],
|
||||
};
|
||||
|
||||
resetFilter(
|
||||
mockAutomation,
|
||||
mockAutomationTypes,
|
||||
0,
|
||||
mockAutomation.conditions[0]
|
||||
);
|
||||
resetAction(mockAutomation, 0);
|
||||
resetFilter(0, automation.value.conditions[0]);
|
||||
resetAction(0);
|
||||
|
||||
expect(mockAutomation.conditions[0].filter_operator).toBe('not_equal_to');
|
||||
expect(mockAutomation.conditions[0].values).toBe('');
|
||||
expect(mockAutomation.actions[0].action_params).toEqual([]);
|
||||
});
|
||||
|
||||
it('formats automation correctly', () => {
|
||||
const { formatAutomation } = useAutomation();
|
||||
const mockAutomation = {
|
||||
conditions: [{ attribute_key: 'status', values: ['open'] }],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {};
|
||||
const mockAutomationActionTypes = [
|
||||
{ key: 'assign_agent', inputType: 'search_select' },
|
||||
];
|
||||
|
||||
automationHelper.getConditionOptions.mockReturnValue([
|
||||
{ id: 'open', name: 'open' },
|
||||
]);
|
||||
automationHelper.getActionOptions.mockReturnValue([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
|
||||
const result = formatAutomation(
|
||||
mockAutomation,
|
||||
customAttributes,
|
||||
mockAutomationTypes,
|
||||
mockAutomationActionTypes
|
||||
);
|
||||
|
||||
expect(result.conditions[0].values).toEqual([{ id: 'open', name: 'open' }]);
|
||||
expect(result.actions[0].action_params).toEqual([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
expect(automation.value.conditions[0].filter_operator).toBe('not_equal_to');
|
||||
expect(automation.value.conditions[0].values).toBe('');
|
||||
expect(automation.value.actions[0].action_params).toEqual([]);
|
||||
});
|
||||
|
||||
it('manifests custom attributes correctly', () => {
|
||||
const { manifestCustomAttributes } = useAutomation();
|
||||
const mockAutomationTypes = {
|
||||
message_created: { conditions: [] },
|
||||
conversation_created: { conditions: [] },
|
||||
conversation_updated: { conditions: [] },
|
||||
conversation_opened: { conditions: [] },
|
||||
};
|
||||
const { manifestCustomAttributes, automationTypes } = useAutomation();
|
||||
automationTypes.message_created = { conditions: [] };
|
||||
automationTypes.conversation_created = { conditions: [] };
|
||||
automationTypes.conversation_updated = { conditions: [] };
|
||||
automationTypes.conversation_opened = { conditions: [] };
|
||||
|
||||
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
|
||||
automationHelper.generateCustomAttributes.mockReturnValue([]);
|
||||
|
||||
manifestCustomAttributes(mockAutomationTypes);
|
||||
manifestCustomAttributes();
|
||||
|
||||
expect(automationHelper.generateCustomAttributeTypes).toHaveBeenCalledTimes(
|
||||
2
|
||||
);
|
||||
expect(automationHelper.generateCustomAttributes).toHaveBeenCalledTimes(1);
|
||||
Object.values(mockAutomationTypes).forEach(type => {
|
||||
Object.values(automationTypes).forEach(type => {
|
||||
expect(type.conditions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -273,8 +234,8 @@ describe('useAutomation', () => {
|
||||
});
|
||||
|
||||
it('handles event change correctly', () => {
|
||||
const { onEventChange } = useAutomation();
|
||||
const mockAutomation = {
|
||||
const { onEventChange, automation } = useAutomation();
|
||||
automation.value = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
@@ -283,13 +244,13 @@ describe('useAutomation', () => {
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
onEventChange(mockAutomation);
|
||||
onEventChange();
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
expect(automation.value.conditions).toHaveLength(1);
|
||||
expect(automation.value.actions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,313 +1,143 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
import {
|
||||
generateCustomAttributeTypes,
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
getCustomAttributeInputType,
|
||||
getDefaultConditions,
|
||||
getDefaultActions,
|
||||
filterCustomAttributes,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
generateCustomAttributes,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import useAutomationValues from './useAutomationValues';
|
||||
|
||||
import {
|
||||
// AUTOMATION_RULE_EVENTS,
|
||||
// AUTOMATION_ACTION_TYPES,
|
||||
AUTOMATIONS,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/constants.js';
|
||||
|
||||
/**
|
||||
* Composable for handling automation-related functionality.
|
||||
* @returns {Object} An object containing various automation-related functions and computed properties.
|
||||
*/
|
||||
export function useAutomation() {
|
||||
export function useAutomation(startValue = null) {
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agents = useMapGetter('agents/getAgents');
|
||||
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
||||
const contacts = useMapGetter('contacts/getContacts');
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
const teams = useMapGetter('teams/getTeams');
|
||||
const slaPolicies = useMapGetter('sla/getSLA');
|
||||
const {
|
||||
booleanFilterOptions,
|
||||
statusFilterOptions,
|
||||
getConditionDropdownValues,
|
||||
getActionDropdownValues,
|
||||
agents,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
} = useAutomationValues();
|
||||
|
||||
const booleanFilterOptions = computed(() => [
|
||||
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
|
||||
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
|
||||
]);
|
||||
const automation = ref(startValue);
|
||||
const automationTypes = structuredClone(AUTOMATIONS);
|
||||
const eventName = computed(() => automation.value?.event_name);
|
||||
|
||||
const statusFilterItems = computed(() => {
|
||||
return {
|
||||
open: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
|
||||
},
|
||||
resolved: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
|
||||
},
|
||||
pending: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
|
||||
},
|
||||
snoozed: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
|
||||
},
|
||||
all: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
|
||||
},
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Handles the event change for an automation.value.
|
||||
*/
|
||||
const onEventChange = () => {
|
||||
automation.value.conditions = getDefaultConditions(eventName.value);
|
||||
automation.value.actions = getDefaultActions();
|
||||
};
|
||||
|
||||
const statusFilterOptions = computed(() => {
|
||||
const statusFilters = statusFilterItems.value;
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => ({
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
})),
|
||||
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
|
||||
/**
|
||||
* Appends a new condition to the automation.value.
|
||||
*/
|
||||
const appendNewCondition = () => {
|
||||
const defaultCondition = getDefaultConditions(eventName.value);
|
||||
automation.value.conditions = [
|
||||
...automation.value.conditions,
|
||||
...defaultCondition,
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the event change for an automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const onEventChange = automation => {
|
||||
automation.conditions = getDefaultConditions(automation.event_name);
|
||||
automation.actions = getDefaultActions();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the condition dropdown values for a given type.
|
||||
* @param {string} type - The type of condition.
|
||||
* @returns {Array} An array of condition dropdown values.
|
||||
* Appends a new action to the automation.value.
|
||||
*/
|
||||
const getConditionDropdownValues = type => {
|
||||
return getConditionOptions({
|
||||
agents: agents.value,
|
||||
booleanFilterOptions: booleanFilterOptions.value,
|
||||
campaigns: campaigns.value,
|
||||
contacts: contacts.value,
|
||||
customAttributes: getters['attributes/getAttributes'].value,
|
||||
inboxes: inboxes.value,
|
||||
statusFilterOptions: statusFilterOptions.value,
|
||||
teams: teams.value,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
const appendNewAction = () => {
|
||||
const defaultAction = getDefaultActions();
|
||||
automation.value.actions = [...automation.value.actions, ...defaultAction];
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new condition to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewCondition = automation => {
|
||||
automation.conditions.push(...getDefaultConditions(automation.event_name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new action to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewAction = automation => {
|
||||
automation.actions.push(...getDefaultActions());
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a filter from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* Removes a filter from the automation.value.
|
||||
* @param {number} index - The index of the filter to remove.
|
||||
*/
|
||||
const removeFilter = (automation, index) => {
|
||||
if (automation.conditions.length <= 1) {
|
||||
const removeFilter = index => {
|
||||
if (automation.value.conditions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.conditions.splice(index, 1);
|
||||
automation.value.conditions = automation.value.conditions.filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an action from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* Removes an action from the automation.value.
|
||||
* @param {number} index - The index of the action to remove.
|
||||
*/
|
||||
const removeAction = (automation, index) => {
|
||||
if (automation.actions.length <= 1) {
|
||||
const removeAction = index => {
|
||||
if (automation.value.actions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.actions.splice(index, 1);
|
||||
automation.value.actions = automation.value.actions.filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets a filter in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* Resets a filter in the automation.value.
|
||||
* @param {Object} automationTypes - The automation types object.
|
||||
* @param {number} index - The index of the filter to reset.
|
||||
* @param {Object} currentCondition - The current condition object.
|
||||
*/
|
||||
const resetFilter = (
|
||||
automation,
|
||||
automationTypes,
|
||||
index,
|
||||
currentCondition
|
||||
) => {
|
||||
automation.conditions[index].filter_operator = automationTypes[
|
||||
automation.event_name
|
||||
].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value;
|
||||
automation.conditions[index].values = '';
|
||||
const resetFilter = (index, currentCondition) => {
|
||||
const newConditions = [...automation.value.conditions];
|
||||
|
||||
newConditions[index] = {
|
||||
...newConditions[index],
|
||||
filter_operator: automationTypes[eventName.value].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value,
|
||||
values: '',
|
||||
};
|
||||
|
||||
automation.value.conditions = newConditions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets an action in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* Resets an action in the automation.value.
|
||||
* @param {number} index - The index of the action to reset.
|
||||
*/
|
||||
const resetAction = (automation, index) => {
|
||||
automation.actions[index].action_params = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the conditions for automation.
|
||||
* It help to format the conditions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing conditions to manifest.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @returns {Array} An array of manifested conditions.
|
||||
*/
|
||||
const manifestConditions = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
) => {
|
||||
const customAttributes = filterCustomAttributes(allCustomAttributes);
|
||||
return automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
automationTypes,
|
||||
automation.event_name,
|
||||
condition.attribute_key
|
||||
);
|
||||
}
|
||||
if (inputType === 'plain_text' || inputType === 'date') {
|
||||
return { ...condition, values: condition.values[0] };
|
||||
}
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return { ...condition, values: condition.values.join(',') };
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
|
||||
item => [...condition.values].includes(item.id)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the action dropdown values for a given type.
|
||||
* @param {string} type - The type of action.
|
||||
* @returns {Array} An array of action dropdown values.
|
||||
*/
|
||||
const getActionDropdownValues = type => {
|
||||
return getActionOptions({
|
||||
agents: agents.value,
|
||||
labels: labels.value,
|
||||
teams: teams.value,
|
||||
slaPolicies: slaPolicies.value,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of actions for the automation.
|
||||
* @param {Object} action - The action object.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array|Object} Generated actions array or object based on input type.
|
||||
*/
|
||||
const generateActionsArray = (action, automationActionTypes) => {
|
||||
const params = action.action_params;
|
||||
const inputType = automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
return [...getActionDropdownValues(action.action_name)].filter(item =>
|
||||
[...params].includes(item.id)
|
||||
);
|
||||
}
|
||||
if (inputType === 'team_message') {
|
||||
return {
|
||||
team_ids: [...getActionDropdownValues(action.action_name)].filter(
|
||||
item => [...params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: params[0].message,
|
||||
};
|
||||
}
|
||||
return [...params];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the actions for automation.
|
||||
* It help to format the actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing actions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array} An array of manifested actions.
|
||||
*/
|
||||
const manifestActions = (automation, automationActionTypes) => {
|
||||
return automation.actions.map(action => ({
|
||||
...action,
|
||||
action_params: action.action_params.length
|
||||
? generateActionsArray(action, automationActionTypes)
|
||||
: [],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the automation object for use when we edit the automation.
|
||||
* It help to format the conditions and actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object to format.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
|
||||
*/
|
||||
const formatAutomation = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automationActionTypes
|
||||
) => {
|
||||
return {
|
||||
...automation,
|
||||
conditions: manifestConditions(
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
),
|
||||
actions: manifestActions(automation, automationActionTypes),
|
||||
const resetAction = index => {
|
||||
const newActions = [...automation.value.actions];
|
||||
newActions[index] = {
|
||||
...newActions[index],
|
||||
action_params: [],
|
||||
};
|
||||
|
||||
automation.value.actions = newActions;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function formats the custom attributes for automation types.
|
||||
* It retrieves custom attributes for conversations and contacts,
|
||||
* generates custom attribute types, and adds them to the relevant automation types.
|
||||
* @param {Object} automationTypes - The automation types object to update with custom attributes.
|
||||
*/
|
||||
const manifestCustomAttributes = automationTypes => {
|
||||
const manifestCustomAttributes = () => {
|
||||
const conversationCustomAttributesRaw = getters[
|
||||
'attributes/getAttributesByModel'
|
||||
].value('conversation_attribute');
|
||||
@@ -330,21 +160,22 @@ export function useAutomation() {
|
||||
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||
);
|
||||
|
||||
automationTypes.message_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_updated.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_opened.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
[
|
||||
'message_created',
|
||||
'conversation_created',
|
||||
'conversation_updated',
|
||||
'conversation_opened',
|
||||
].forEach(eventToUpdate => {
|
||||
automationTypes[eventToUpdate].conditions = [
|
||||
...automationTypes[eventToUpdate].conditions,
|
||||
...manifestedCustomAttributes,
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
automation,
|
||||
automationTypes,
|
||||
agents,
|
||||
campaigns,
|
||||
contacts,
|
||||
@@ -362,7 +193,6 @@ export function useAutomation() {
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
formatAutomation,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import {
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
|
||||
/**
|
||||
* This is a shared composables that holds utilites used to build dropdown and file options
|
||||
* @returns {Object} An object containing various automation-related functions and computed properties.
|
||||
*/
|
||||
export default function useAutomationValues() {
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agents = useMapGetter('agents/getAgents');
|
||||
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
||||
const contacts = useMapGetter('contacts/getContacts');
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
const teams = useMapGetter('teams/getTeams');
|
||||
const slaPolicies = useMapGetter('sla/getSLA');
|
||||
|
||||
const booleanFilterOptions = computed(() => [
|
||||
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
|
||||
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
|
||||
]);
|
||||
|
||||
const statusFilterItems = computed(() => {
|
||||
return {
|
||||
open: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
|
||||
},
|
||||
resolved: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
|
||||
},
|
||||
pending: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
|
||||
},
|
||||
snoozed: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
|
||||
},
|
||||
all: {
|
||||
TEXT: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const statusFilterOptions = computed(() => {
|
||||
const statusFilters = statusFilterItems.value;
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => ({
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
})),
|
||||
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets the condition dropdown values for a given type.
|
||||
* @param {string} type - The type of condition.
|
||||
* @returns {Array} An array of condition dropdown values.
|
||||
*/
|
||||
const getConditionDropdownValues = type => {
|
||||
return getConditionOptions({
|
||||
agents: agents.value,
|
||||
booleanFilterOptions: booleanFilterOptions.value,
|
||||
campaigns: campaigns.value,
|
||||
contacts: contacts.value,
|
||||
customAttributes: getters['attributes/getAttributes'].value,
|
||||
inboxes: inboxes.value,
|
||||
statusFilterOptions: statusFilterOptions.value,
|
||||
teams: teams.value,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the action dropdown values for a given type.
|
||||
* @param {string} type - The type of action.
|
||||
* @returns {Array} An array of action dropdown values.
|
||||
*/
|
||||
const getActionDropdownValues = type => {
|
||||
return getActionOptions({
|
||||
agents: agents.value,
|
||||
labels: labels.value,
|
||||
teams: teams.value,
|
||||
slaPolicies: slaPolicies.value,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
booleanFilterOptions,
|
||||
statusFilterItems,
|
||||
statusFilterOptions,
|
||||
getConditionDropdownValues,
|
||||
getActionDropdownValues,
|
||||
agents,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import useAutomationValues from './useAutomationValues';
|
||||
|
||||
import {
|
||||
getCustomAttributeInputType,
|
||||
filterCustomAttributes,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
|
||||
export function useEditableAutomation() {
|
||||
const { getConditionDropdownValues, getActionDropdownValues } =
|
||||
useAutomationValues();
|
||||
|
||||
/**
|
||||
* This function sets the conditions for automation.
|
||||
* It help to format the conditions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing conditions to manifest.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @returns {Array} An array of manifested conditions.
|
||||
*/
|
||||
const manifestConditions = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
) => {
|
||||
const customAttributes = filterCustomAttributes(allCustomAttributes);
|
||||
return automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
automationTypes,
|
||||
automation.event_name,
|
||||
condition.attribute_key
|
||||
);
|
||||
}
|
||||
if (inputType === 'plain_text' || inputType === 'date') {
|
||||
return { ...condition, values: condition.values[0] };
|
||||
}
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return { ...condition, values: condition.values.join(',') };
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
|
||||
item => [...condition.values].includes(item.id)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of actions for the automation.
|
||||
* @param {Object} action - The action object.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array|Object} Generated actions array or object based on input type.
|
||||
*/
|
||||
const generateActionsArray = (action, automationActionTypes) => {
|
||||
const params = action.action_params;
|
||||
const inputType = automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
return [...getActionDropdownValues(action.action_name)].filter(item =>
|
||||
[...params].includes(item.id)
|
||||
);
|
||||
}
|
||||
if (inputType === 'team_message') {
|
||||
return {
|
||||
team_ids: [...getActionDropdownValues(action.action_name)].filter(
|
||||
item => [...params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: params[0].message,
|
||||
};
|
||||
}
|
||||
return [...params];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the actions for automation.
|
||||
* It help to format the actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing actions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array} An array of manifested actions.
|
||||
*/
|
||||
const manifestActions = (automation, automationActionTypes) => {
|
||||
return automation.actions.map(action => ({
|
||||
...action,
|
||||
action_params: action.action_params.length
|
||||
? generateActionsArray(action, automationActionTypes)
|
||||
: [],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the automation object for use when we edit the automation.
|
||||
* It help to format the conditions and actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object to format.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
|
||||
*/
|
||||
const formatAutomation = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automationActionTypes
|
||||
) => {
|
||||
return {
|
||||
...automation,
|
||||
conditions: manifestConditions(
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
),
|
||||
actions: manifestActions(automation, automationActionTypes),
|
||||
};
|
||||
};
|
||||
|
||||
return { formatAutomation };
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import plugin from '../plugin';
|
||||
import analyticsHelper from '../index';
|
||||
|
||||
vi.spyOn(analyticsHelper, 'init');
|
||||
vi.spyOn(analyticsHelper, 'track');
|
||||
|
||||
describe('Vue Analytics Plugin', () => {
|
||||
beforeEach(() => {
|
||||
Vue.use(plugin);
|
||||
});
|
||||
|
||||
it('should call the init method on analyticsHelper once during plugin installation', () => {
|
||||
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
|
||||
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
|
||||
});
|
||||
|
||||
it('should add a track method to the Vue prototype as $track', () => {
|
||||
expect(typeof Vue.prototype.$track).toBe('function');
|
||||
Vue.prototype.$track('eventName');
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith('eventName');
|
||||
});
|
||||
|
||||
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
|
||||
const eventName = 'testEvent';
|
||||
Vue.prototype.$track(eventName);
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith(eventName);
|
||||
});
|
||||
});
|
||||
@@ -39,13 +39,13 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
}
|
||||
|
||||
setInstanceValues({
|
||||
setInstanceValues = ({
|
||||
currentUser,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType,
|
||||
audioAlertTone,
|
||||
}) {
|
||||
}) => {
|
||||
this.audioAlertType = audioAlertType;
|
||||
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
@@ -58,9 +58,9 @@ class DashboardAudioNotificationHelper {
|
||||
});
|
||||
});
|
||||
initFaviconSwitcher();
|
||||
}
|
||||
};
|
||||
|
||||
executeRecurringNotification() {
|
||||
executeRecurringNotification = () => {
|
||||
if (!window.WOOT_STORE) {
|
||||
this.clearSetTimeout();
|
||||
return;
|
||||
@@ -81,9 +81,9 @@ class DashboardAudioNotificationHelper {
|
||||
showBadgeOnFavicon();
|
||||
}
|
||||
this.clearSetTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
clearSetTimeout() {
|
||||
clearSetTimeout = () => {
|
||||
if (this.recurringNotificationTimer) {
|
||||
clearTimeout(this.recurringNotificationTimer);
|
||||
}
|
||||
@@ -91,9 +91,9 @@ class DashboardAudioNotificationHelper {
|
||||
this.executeRecurringNotification,
|
||||
NOTIFICATION_TIME
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
playAudioEvery30Seconds() {
|
||||
playAudioEvery30Seconds = () => {
|
||||
// Audio alert is disabled dismiss the timer
|
||||
if (this.audioAlertType === 'none') {
|
||||
return;
|
||||
@@ -104,25 +104,25 @@ class DashboardAudioNotificationHelper {
|
||||
}
|
||||
|
||||
this.clearSetTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
isConversationAssignedToCurrentUser(message) {
|
||||
isConversationAssignedToCurrentUser = message => {
|
||||
const conversationAssigneeId = message?.conversation?.assignee_id;
|
||||
return conversationAssigneeId === this.currentUserId;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isMessageFromCurrentConversation(message) {
|
||||
isMessageFromCurrentConversation = message => {
|
||||
return (
|
||||
window.WOOT_STORE.getters.getSelectedChat?.id === message.conversation_id
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
isMessageFromCurrentUser(message) {
|
||||
isMessageFromCurrentUser = message => {
|
||||
return message?.sender_id === this.currentUserId;
|
||||
}
|
||||
};
|
||||
|
||||
isUserHasConversationPermission() {
|
||||
isUserHasConversationPermission = () => {
|
||||
const currentAccountId = window.WOOT_STORE.getters.getCurrentAccountId;
|
||||
// Get the user permissions for the current account
|
||||
const userPermissions = getUserPermissions(
|
||||
@@ -134,16 +134,16 @@ class DashboardAudioNotificationHelper {
|
||||
permission => userPermissions.includes(permission)
|
||||
);
|
||||
return hasRequiredPermission;
|
||||
}
|
||||
};
|
||||
|
||||
shouldNotifyOnMessage(message) {
|
||||
shouldNotifyOnMessage = message => {
|
||||
if (this.audioAlertType === 'mine') {
|
||||
return this.isConversationAssignedToCurrentUser(message);
|
||||
}
|
||||
return this.audioAlertType === 'all';
|
||||
}
|
||||
};
|
||||
|
||||
onNewMessage(message) {
|
||||
onNewMessage = message => {
|
||||
// If the user does not have the permission to view the conversation, then dismiss the alert
|
||||
if (!this.isUserHasConversationPermission()) {
|
||||
return;
|
||||
@@ -176,7 +176,7 @@ class DashboardAudioNotificationHelper {
|
||||
window.playAudioAlert();
|
||||
showBadgeOnFavicon();
|
||||
this.playAudioEvery30Seconds();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const notifHelper = new DashboardAudioNotificationHelper();
|
||||
|
||||
@@ -37,8 +37,10 @@ const storeMock = {
|
||||
|
||||
const routerMock = {
|
||||
currentRoute: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
value: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -222,7 +224,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('fetchConversationMessagesOnReconnect', () => {
|
||||
it('should dispatch syncActiveConversationMessages if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
@@ -231,7 +233,7 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
@@ -305,7 +307,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('setConversationLastMessageId', () => {
|
||||
it('should dispatch setConversationLastMessageId if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
@@ -314,7 +316,7 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import resize from '../../directives/resize';
|
||||
|
||||
class ResizeObserverMock {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
observe() {}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
unobserve() {}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
describe('resize directive', () => {
|
||||
let el;
|
||||
let binding;
|
||||
let observer;
|
||||
|
||||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: vi.fn(),
|
||||
};
|
||||
observer = {
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
window.ResizeObserver = ResizeObserverMock;
|
||||
vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create ResizeObserver on bind', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
expect(ResizeObserver).toHaveBeenCalled();
|
||||
expect(observer.observe).toHaveBeenCalledWith(el);
|
||||
});
|
||||
|
||||
it('should call callback on observer callback', () => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: vi.fn(),
|
||||
};
|
||||
|
||||
resize.bind(el, binding);
|
||||
|
||||
const entries = [{ contentRect: { width: 100, height: 100 } }];
|
||||
const callback = binding.value;
|
||||
callback(entries[0]);
|
||||
|
||||
expect(binding.value).toHaveBeenCalledWith(entries[0]);
|
||||
});
|
||||
|
||||
it('should destroy and recreate observer on update', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
resize.update(el, { ...binding, oldValue: 'old' });
|
||||
|
||||
expect(observer.unobserve).toHaveBeenCalledWith(el);
|
||||
expect(observer.disconnect).toHaveBeenCalled();
|
||||
expect(ResizeObserver).toHaveBeenCalledTimes(2);
|
||||
expect(observer.observe).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should destroy observer on unbind', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
resize.unbind(el);
|
||||
|
||||
expect(observer.unobserve).toHaveBeenCalledWith(el);
|
||||
expect(observer.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
} from '../editorHelper';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { EditorState } from '@chatwoot/prosemirror-schema';
|
||||
import { EditorView } from '@chatwoot/prosemirror-schema';
|
||||
import { Schema } from 'prosemirror-model';
|
||||
|
||||
// Define a basic ProseMirror schema
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure want to delete - %{attributeName}",
|
||||
"TITLE": "Are you sure want to delete - {attributeName}",
|
||||
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
||||
"MESSAGE": "Deleting will remove the custom attribute",
|
||||
"YES": "Delete ",
|
||||
|
||||
@@ -23,52 +23,52 @@
|
||||
},
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} created a new automation rule (#%{id})",
|
||||
"EDIT": "%{agentName} updated an automation rule (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
|
||||
"ADD": "{agentName} created a new automation rule (#{id})",
|
||||
"EDIT": "{agentName} updated an automation rule (#{id})",
|
||||
"DELETE": "{agentName} deleted an automation rule (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
|
||||
"ADD": "{agentName} invited {invitee} to the account as an {role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} changed their %{attributes} to %{values}",
|
||||
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
|
||||
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
|
||||
"SELF": "{agentName} changed their {attributes} to {values}",
|
||||
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
|
||||
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} created a new inbox (#%{id})",
|
||||
"EDIT": "%{agentName} updated an inbox (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an inbox (#%{id})"
|
||||
"ADD": "{agentName} created a new inbox (#{id})",
|
||||
"EDIT": "{agentName} updated an inbox (#{id})",
|
||||
"DELETE": "{agentName} deleted an inbox (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} created a new webhook (#%{id})",
|
||||
"EDIT": "%{agentName} updated a webhook (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a webhook (#%{id})"
|
||||
"ADD": "{agentName} created a new webhook (#{id})",
|
||||
"EDIT": "{agentName} updated a webhook (#{id})",
|
||||
"DELETE": "{agentName} deleted a webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} signed in",
|
||||
"SIGN_OUT": "%{agentName} signed out"
|
||||
"SIGN_IN": "{agentName} signed in",
|
||||
"SIGN_OUT": "{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} created a new team (#%{id})",
|
||||
"EDIT": "%{agentName} updated a team (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a team (#%{id})"
|
||||
"ADD": "{agentName} created a new team (#{id})",
|
||||
"EDIT": "{agentName} updated a team (#{id})",
|
||||
"DELETE": "{agentName} deleted a team (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} created a new macro (#%{id})",
|
||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||
"ADD": "{agentName} created a new macro (#{id})",
|
||||
"EDIT": "{agentName} updated a macro (#{id})",
|
||||
"DELETE": "{agentName} deleted a macro (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||
"ADD": "{agentName} added {user} to the team(#{team_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"YES": "Yes",
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
|
||||
"DELETE_WARNING": "Contact of <strong>{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"SELECT_PLACEHOLDER": "None",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"NO_RESULTS": "No results found",
|
||||
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
|
||||
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
}
|
||||
},
|
||||
@@ -122,15 +122,15 @@
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"",
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
"FAILED": "Couldn't assign agent. Please try again."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned label #%{labelName} to conversation id %{conversationId}",
|
||||
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign label. Please try again."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}",
|
||||
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign team. Please try again."
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"TITLE": "Hey 👋, Welcome to {installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of {installationName}. Here are a few things you can do in {installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
@@ -317,10 +317,10 @@
|
||||
"SIDEBAR_TITLE": "Conversation participants",
|
||||
"NO_RECORDS_FOUND": "No results found",
|
||||
"ADD_PARTICIPANTS": "Select participants",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
|
||||
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
|
||||
"WATCH_CONVERSATION": "Join conversation",
|
||||
"YOU_ARE_WATCHING": "You are participating",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"LEARN_MORE": "Learn more",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"ARTICLE_SEARCH_RESULT": {
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"SEARCH_RESULTS": "Search results for %{query}",
|
||||
"SEARCH_RESULTS": "Search results for {query}",
|
||||
"EMPTY_TEXT": "Search for articles to insert into replies.",
|
||||
"SEARCH_LOADER": "Searching...",
|
||||
"INSERT_ARTICLE": "Insert",
|
||||
@@ -250,13 +250,13 @@
|
||||
"DOMAIN": {
|
||||
"LABEL": "Custom Domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: %{exampleURL}",
|
||||
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: {exampleURL}",
|
||||
"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. Eg: %{exampleURL}",
|
||||
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: {exampleURL}",
|
||||
"ERROR": "Enter a valid home page URL"
|
||||
},
|
||||
"THEME_COLOR": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
@@ -56,7 +56,7 @@
|
||||
"BUTTON_TEXT": "Disconnect"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with {installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: %{webhookExampleURL}",
|
||||
"PLACEHOLDER": "Example: {webhookExampleURL}",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"EDIT_SUBMIT": "Update webhook",
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
|
||||
"MESSAGE": "Are you sure to delete the webhook? ({webhookURL})",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "How to use the Slack Integration?",
|
||||
"BODY": "With this integration, all of your incoming conversations will be synced to the ***%{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***%{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
|
||||
"BODY": "With this integration, all of your incoming conversations will be synced to the ***{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
|
||||
"SELECTED": "selected"
|
||||
},
|
||||
"SELECT_CHANNEL": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"OPEN_AI": {
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"WITH_AI": " %{option} with AI ",
|
||||
"WITH_AI": " {option} with AI ",
|
||||
"OPTIONS": {
|
||||
"REPLY_SUGGESTION": "Reply Suggestion",
|
||||
"SUMMARIZE": "Summarize",
|
||||
@@ -216,7 +216,7 @@
|
||||
"CONFIRM_YES": "Yes, delete it",
|
||||
"CONFIRM_NO": "No, keep it",
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete the app - %{appName}?",
|
||||
"MESSAGE": "Are you sure to delete the app - {appName}?",
|
||||
"API_SUCCESS": "Dashboard app deleted successfully",
|
||||
"API_ERROR": "We couldn't delete the app. Please try again later"
|
||||
}
|
||||
@@ -235,7 +235,7 @@
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
"LINK_TITLE": "Conversation (#{conversationId}) with {name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
@@ -294,7 +294,7 @@
|
||||
"PRIORITY": "Priority",
|
||||
"ASSIGNEE": "Assignee",
|
||||
"LABELS": "Labels",
|
||||
"CREATED_AT": "Created at %{createdAt}"
|
||||
"CREATED_AT": "Created at {createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Unlink",
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"NAME": "Customer waiting time",
|
||||
"TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)"
|
||||
"TOOLTIP_TEXT": "Waiting time is {metricValue} (based on {conversationCount} replies)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
@@ -123,13 +123,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -190,13 +190,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -257,13 +257,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -324,13 +324,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -436,8 +436,8 @@
|
||||
"CONVERSATION_HEATMAP": {
|
||||
"HEADER": "Conversation Traffic",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "%{count} conversation",
|
||||
"CONVERSATIONS": "%{count} conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"MESSAGES": "Messages"
|
||||
},
|
||||
"SEARCHING_DATA": "Searching",
|
||||
"EMPTY_STATE": "No %{item} found for query '%{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
"TITLE": "Billing",
|
||||
"CURRENT_PLAN": {
|
||||
"TITLE": "Current Plan",
|
||||
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
|
||||
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
|
||||
},
|
||||
"MANAGE_SUBSCRIPTION": {
|
||||
"TITLE": "Manage your subscription",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Add agents to team",
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Update agents in team",
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. All the added agents will be notified when a conversation is assigned to this team."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -77,14 +77,14 @@
|
||||
"ADD_AGENTS": "Adding Agents to your Team...",
|
||||
"SELECT": "select",
|
||||
"SELECT_ALL": "select all agents",
|
||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected."
|
||||
"SELECTED_COUNT": "{selected} out of {total} agents selected."
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation.",
|
||||
"SELECT": "select",
|
||||
"SELECT_ALL": "select all agents",
|
||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
|
||||
"SELECTED_COUNT": "{selected} out of {total} agents selected.",
|
||||
"BUTTON_TEXT": "Add agents",
|
||||
"AGENT_VALIDATION_ERROR": "Select at least one agent."
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"ERROR_MESSAGE": "تعذر حذف الصفة المخصصة. حاول مرة أخرى."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "هل أنت متأكد من أنك تريد حذف - %{attributeName}",
|
||||
"TITLE": "هل أنت متأكد من أنك تريد حذف - {attributeName}",
|
||||
"PLACE_HOLDER": "الرجاء كتابة {attributeName} للتأكيد",
|
||||
"MESSAGE": "الحذف سوف يزيل الصفة المخصصة",
|
||||
"YES": "حذف ",
|
||||
|
||||
@@ -23,52 +23,52 @@
|
||||
},
|
||||
"DEFAULT_USER": "النظام",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} أنشأ قاعدة أتمتة جديدة (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث قاعدة أتمتة (#%{id})",
|
||||
"DELETE": "%{agentName} حذف قاعدة أتمتة (#%{id})"
|
||||
"ADD": "{agentName} أنشأ قاعدة أتمتة جديدة (#{id})",
|
||||
"EDIT": "{agentName} قام بتحديث قاعدة أتمتة (#{id})",
|
||||
"DELETE": "{agentName} حذف قاعدة أتمتة (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} دعا %{invitee} إلى الحساب كـ %{role}",
|
||||
"ADD": "{agentName} دعا {invitee} إلى الحساب كـ {role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
|
||||
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}",
|
||||
"DELETED": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
|
||||
"SELF": "{agentName} غير {attributes} الخاصة به إلى {values}",
|
||||
"OTHER": "{agentName} غير {attributes} لـ {user} إلى {values}",
|
||||
"DELETED": "{agentName} غير {attributes} لـ {user} إلى {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} أنشأ صندوق وارد جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث صندوق الوارد (#%{id})",
|
||||
"DELETE": "%{agentName} حذف صندوق الوارد (#%{id})"
|
||||
"ADD": "{agentName} أنشأ صندوق وارد جديد (#{id})",
|
||||
"EDIT": "{agentName} قام بتحديث صندوق الوارد (#{id})",
|
||||
"DELETE": "{agentName} حذف صندوق الوارد (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} أنشأ Webhook جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث Webhook (#%{id})",
|
||||
"DELETE": "%{agentName} حذف Webhook (#%{id})"
|
||||
"ADD": "{agentName} أنشأ Webhook جديد (#{id})",
|
||||
"EDIT": "{agentName} قام بتحديث Webhook (#{id})",
|
||||
"DELETE": "{agentName} حذف Webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} قام بتسجيل الدخول",
|
||||
"SIGN_OUT": "%{agentName} قام بتسجيل الخروج"
|
||||
"SIGN_IN": "{agentName} قام بتسجيل الدخول",
|
||||
"SIGN_OUT": "{agentName} قام بتسجيل الخروج"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} أنشأ فريق جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث الفريق (#%{id})",
|
||||
"DELETE": "%{agentName} حذف الفريق (#%{id})"
|
||||
"ADD": "{agentName} أنشأ فريق جديد (#{id})",
|
||||
"EDIT": "{agentName} قام بتحديث الفريق (#{id})",
|
||||
"DELETE": "{agentName} حذف الفريق (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} أنشأ ماكرو جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث ماكرو (#%{id})",
|
||||
"DELETE": "%{agentName} حذف ماكرو (#%{id})"
|
||||
"ADD": "{agentName} أنشأ ماكرو جديد (#{id})",
|
||||
"EDIT": "{agentName} قام بتحديث ماكرو (#{id})",
|
||||
"DELETE": "{agentName} حذف ماكرو (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} أضاف %{user} إلى صندوق الوارد (#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} أزال %{user} من صندوق الوارد (#%{inbox_id})"
|
||||
"ADD": "{agentName} أضاف {user} إلى صندوق الوارد (#{inbox_id})",
|
||||
"REMOVE": "{agentName} أزال {user} من صندوق الوارد (#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} أضاف %{user} إلى الفريق (#%{team_id})",
|
||||
"REMOVE": "%{agentName} أزال %{user} من الفريق (#%{team_id})"
|
||||
"ADD": "{agentName} أضاف {user} إلى الفريق (#{team_id})",
|
||||
"REMOVE": "{agentName} أزال {user} من الفريق (#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} قام بتحديث إعدادات الحساب (#%{id})"
|
||||
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (#{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} المحادثات المحددة",
|
||||
"AGENT_SELECT_LABEL": "اختر وكيل",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من إلغاء تعيين %{conversationCount} %{conversationLabel}؟",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين {conversationCount} {conversationLabel} إلى",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من إلغاء تعيين {conversationCount} {conversationLabel}؟",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"ASSIGN_LABEL": "تكليف",
|
||||
"YES": "نعم",
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "ملخص",
|
||||
"DELETE_WARNING": "سيتم حذف جهة الاتصال <strong>%{primaryContactName}</strong>.",
|
||||
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال من <strong>%{primaryContactName}</strong> إلى <strong>%{parentContactName}</strong>."
|
||||
"DELETE_WARNING": "سيتم حذف جهة الاتصال <strong>{primaryContactName}</strong>.",
|
||||
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال من <strong>{primaryContactName}</strong> إلى <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "رسالة_خطأ"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"SELECT_PLACEHOLDER": "لا يوجد",
|
||||
"INPUT_PLACEHOLDER": "تحديد الأولوية",
|
||||
"NO_RESULTS": "لم يتم العثور على النتائج",
|
||||
"SUCCESSFUL": "تغيير أولوية معرف المحادثة %{conversationId} إلى %{priority}",
|
||||
"SUCCESSFUL": "تغيير أولوية معرف المحادثة {conversationId} إلى {priority}",
|
||||
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
|
||||
}
|
||||
},
|
||||
@@ -122,15 +122,15 @@
|
||||
"ASSIGN_TEAM": "تعيين فريق",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "معرف المحادثة %{conversationId} تم تعيينه لـ \"%{agentName}\"",
|
||||
"SUCCESFUL": "معرف المحادثة {conversationId} تم تعيينه لـ \"{agentName}\"",
|
||||
"FAILED": "تعذر تعيين الوكيل. الرجاء المحاولة مرة أخرى."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "تعيين تسمية #%{labelName} لمعرف المحادثة %{conversationId}",
|
||||
"SUCCESFUL": "تعيين تسمية #{labelName} لمعرف المحادثة {conversationId}",
|
||||
"FAILED": "تعذر تعيين التسمية. الرجاء المحاولة مرة أخرى."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "الفريق المعين \"%{team}\" لمعرف المحادثة %{conversationId}",
|
||||
"SUCCESFUL": "الفريق المعين \"{team}\" لمعرف المحادثة {conversationId}",
|
||||
"FAILED": "تعذر تعيين الفريق. الرجاء المحاولة مرة أخرى."
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "اهلاً 👋، مرحباً بك في %{installationName}!",
|
||||
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من %{installationName}. إليك بعض الأشياء التي يمكنك القيام بها في %{installationName} لجعل التجربة رائعة.",
|
||||
"GREETING_MORNING": "👋 صباح الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"GREETING_EVENING": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"TITLE": "اهلاً 👋، مرحباً بك في {installationName}!",
|
||||
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من {installationName}. إليك بعض الأشياء التي يمكنك القيام بها في {installationName} لجعل التجربة رائعة.",
|
||||
"GREETING_MORNING": "👋 صباح الخير، {name}. مرحبا بك في {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 مساء الخير، {name}. مرحبا بك في {installationName}.",
|
||||
"GREETING_EVENING": "👋 مساء الخير، {name}. مرحبا بك في {installationName}.",
|
||||
"READ_LATEST_UPDATES": "اطلع على آخر التحديثات",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "جميع محادثاتك في مكان واحد",
|
||||
@@ -317,10 +317,10 @@
|
||||
"SIDEBAR_TITLE": "المشاركون في المحادثة",
|
||||
"NO_RECORDS_FOUND": "لم يتم العثور على النتائج",
|
||||
"ADD_PARTICIPANTS": "اختر المشاركين",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+%{count} أخرى",
|
||||
"REMANING_PARTICIPANT_TEXT": "+%{count} أخرى",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "%{count} شخص مشارك.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "%{count} شخص مشارك.",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} أخرى",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} أخرى",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} شخص مشارك.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} شخص مشارك.",
|
||||
"NO_PARTICIPANTS_TEXT": "لا أحد يشارك!",
|
||||
"WATCH_CONVERSATION": "الانضمام إلى المحادثة",
|
||||
"YOU_ARE_WATCHING": "أنت مشترك",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "يمكنك تلقي رسائل البريد الإلكتروني في النطاق المخصص الخاص بك الآن."
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
|
||||
"UPDATE_CHATWOOT": "يتوفر تحديث {latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
|
||||
"LEARN_MORE": "اعرف المزيد",
|
||||
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
|
||||
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"ARTICLE_SEARCH_RESULT": {
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"SEARCH_RESULTS": "Search results for %{query}",
|
||||
"SEARCH_RESULTS": "Search results for {query}",
|
||||
"EMPTY_TEXT": "Search for articles to insert into replies.",
|
||||
"SEARCH_LOADER": "يبحث...",
|
||||
"INSERT_ARTICLE": "Insert",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "جلب التكاملات",
|
||||
"NO_HOOK_CONFIGURED": "لا يوجد %{integrationId} تكاملات مكونة في هذا الحساب.",
|
||||
"NO_HOOK_CONFIGURED": "لا يوجد {integrationId} تكاملات مكونة في هذا الحساب.",
|
||||
"HEADER": "التطبيقات",
|
||||
"STATUS": {
|
||||
"ENABLED": "مفعل",
|
||||
@@ -56,7 +56,7 @@
|
||||
"BUTTON_TEXT": "قطع الاتصال"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "تدفق الحوار هو منصة لفهم اللغة الطبيعية التي تجعل من السهل تصميم ودمج واجهة المستخدم للمحادثة في تطبيق الهاتف المحمول الخاص بك، تطبيق الويب، الجهاز، البوت، نظام الاستجابة الصوتية التفاعلي، وما إلى ذلك. <br /> <br /> تكامل تدفق الحوار مع %{installationName} يسمح لك بتكوين بوت تدفق الحوار مع صناديق الوارد الخاصة بك والذي يتيح للبوت التعامل مع الاستفسارات في البداية وتسليمها إلى وكيل عند الحاجة. ويمكن استخدام تدفق البيانات لتأهيل الخيوط وتقليل عبء العمل الملقى على عاتق الوكلاء عن طريق طرح الأسئلة المتكررة وما إلى ذلك. <br /> <br /> لإضافة تدفق الحوار، تحتاج إلى إنشاء حساب خدمة في وحدة تحكم مشروع جوجل الخاص بك ومشاركة بيانات الاعتماد. يرجى الرجوع إلى مستندات Dialogflow للحصول على مزيد من المعلومات."
|
||||
"DIALOGFLOW": "تدفق الحوار هو منصة لفهم اللغة الطبيعية التي تجعل من السهل تصميم ودمج واجهة المستخدم للمحادثة في تطبيق الهاتف المحمول الخاص بك، تطبيق الويب، الجهاز، البوت، نظام الاستجابة الصوتية التفاعلي، وما إلى ذلك. <br /> <br /> تكامل تدفق الحوار مع {installationName} يسمح لك بتكوين بوت تدفق الحوار مع صناديق الوارد الخاصة بك والذي يتيح للبوت التعامل مع الاستفسارات في البداية وتسليمها إلى وكيل عند الحاجة. ويمكن استخدام تدفق البيانات لتأهيل الخيوط وتقليل عبء العمل الملقى على عاتق الوكلاء عن طريق طرح الأسئلة المتكررة وما إلى ذلك. <br /> <br /> لإضافة تدفق الحوار، تحتاج إلى إنشاء حساب خدمة في وحدة تحكم مشروع جوجل الخاص بك ومشاركة بيانات الاعتماد. يرجى الرجوع إلى مستندات Dialogflow للحصول على مزيد من المعلومات."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من حذف webhook؟ (%{webhookURL})",
|
||||
"MESSAGE": "هل أنت متأكد من حذف webhook؟ ({webhookURL})",
|
||||
"YES": "نعم، احذف ",
|
||||
"NO": "لا، احتفظ به"
|
||||
}
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"OPEN_AI": {
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"WITH_AI": " %{option} with AI ",
|
||||
"WITH_AI": " {option} with AI ",
|
||||
"OPTIONS": {
|
||||
"REPLY_SUGGESTION": "Reply Suggestion",
|
||||
"SUMMARIZE": "Summarize",
|
||||
@@ -216,7 +216,7 @@
|
||||
"CONFIRM_YES": "نعم، احذف",
|
||||
"CONFIRM_NO": "لا، احتفظ به",
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من حذف التطبيق - %{appName}؟",
|
||||
"MESSAGE": "هل أنت متأكد من حذف التطبيق - {appName}؟",
|
||||
"API_SUCCESS": "تم حذف تطبيق لوحة التحكم بنجاح",
|
||||
"API_ERROR": "لم نتمكن من حذف التطبيق. الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
@@ -235,7 +235,7 @@
|
||||
"ERROR": "حدث خطأ أثناء جلب المشكلات من Linear، الرجاء المحاولة مرة أخرى",
|
||||
"LINK_SUCCESS": "تم ربط المشكلة بنجاح",
|
||||
"LINK_ERROR": "حدث خطأ أثناء ربط المشكلة، الرجاء المحاولة مرة أخرى",
|
||||
"LINK_TITLE": "محادثة (#%{conversationId}) مع %{name}"
|
||||
"LINK_TITLE": "محادثة (#{conversationId}) مع {name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "إنشاء/رابط مشكلة في Linear",
|
||||
@@ -294,7 +294,7 @@
|
||||
"PRIORITY": "الأولوية",
|
||||
"ASSIGNEE": "المكلَّف",
|
||||
"LABELS": "الوسوم",
|
||||
"CREATED_AT": "تم إنشاؤها في %{createdAt}"
|
||||
"CREATED_AT": "تم إنشاؤها في {createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "إلغاء الربط",
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"NAME": "وقت انتظار العميل",
|
||||
"TOOLTIP_TEXT": "وقت الانتظار هو %{metricValue} (بناء على ردود %{conversationCount})"
|
||||
"TOOLTIP_TEXT": "وقت الانتظار هو {metricValue} (بناء على ردود {conversationCount})"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
@@ -167,13 +167,13 @@
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
@@ -234,13 +234,13 @@
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
@@ -301,13 +301,13 @@
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
@@ -368,13 +368,13 @@
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الرد الأول هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)",
|
||||
"INFO_TEXT": "العدد الإجمالي للمحادثات المستخدمة في الحساب:",
|
||||
"TOOLTIP_TEXT": "وقت الحل هو %{metricValue} (على أساس %{conversationCount} محادثات)"
|
||||
"TOOLTIP_TEXT": "وقت الحل هو {metricValue} (على أساس {conversationCount} محادثات)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
@@ -480,8 +480,8 @@
|
||||
"CONVERSATION_HEATMAP": {
|
||||
"HEADER": "Conversation Traffic",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "%{count} conversation",
|
||||
"CONVERSATIONS": "%{count} conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"MESSAGES": "الرسائل"
|
||||
},
|
||||
"SEARCHING_DATA": "جاري البحث",
|
||||
"EMPTY_STATE": "لم يتم العثور على %{item} للطلب '%{query}'",
|
||||
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '%{query}'",
|
||||
"EMPTY_STATE": "لم يتم العثور على {item} للطلب '{query}'",
|
||||
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ للتركيز",
|
||||
"INPUT_PLACEHOLDER": "أكتب 3 أحرف أو أكثر للبحث",
|
||||
"EMPTY_STATE_DEFAULT": "البحث عن طريق معرف المحادثة أو البريد الإلكتروني أو رقم الهاتف أو الرسائل للحصول على نتائج بحث أفضل. ",
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
"TITLE": "الفواتير",
|
||||
"CURRENT_PLAN": {
|
||||
"TITLE": "الباقة الحالية",
|
||||
"PLAN_NOTE": "أنت مشترك حاليا في باقة**%{plan}** مع تراخيص **%{quantity}**"
|
||||
"PLAN_NOTE": "أنت مشترك حاليا في باقة**{plan}** مع تراخيص **{quantity}**"
|
||||
},
|
||||
"MANAGE_SUBSCRIPTION": {
|
||||
"TITLE": "إدارة الاشتراك الخاص بك",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "إضافة وكيل للفريق",
|
||||
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
|
||||
"TITLE": "إضافة وكلاء للفريق - {teamName}",
|
||||
"DESC": "إضافة وكلاء إلى فريقك الجديد. هذا يتيح لكم العمل كفريق في المحادثات، والحصول على إشعار عن الأحداث الجديدة في نفس المحادثة."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "تحديث الوكلاء في الفريق",
|
||||
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
|
||||
"TITLE": "إضافة وكلاء للفريق - {teamName}",
|
||||
"DESC": "إضافة وكلاء إلى فريقك الذي تم إنشاؤه حديثاً. سيتم إعلام جميع الوكلاء المضافين عند تعيين محادثة لهذا الفريق."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -77,14 +77,14 @@
|
||||
"ADD_AGENTS": "إضافة وكلاء إلى فريقك...",
|
||||
"SELECT": "حدد",
|
||||
"SELECT_ALL": "تحديد جميع الوكلاء",
|
||||
"SELECTED_COUNT": "تم تحديد %{selected} من أصل %{total} وكيل."
|
||||
"SELECTED_COUNT": "تم تحديد {selected} من أصل {total} وكيل."
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة وكلاء للفريق - %{teamName}",
|
||||
"TITLE": "إضافة وكلاء للفريق - {teamName}",
|
||||
"DESC": "إضافة وكلاء إلى فريقك الجديد. هذا يتيح لكم العمل كفريق في المحادثات، والحصول على إشعار عن الأحداث الجديدة في نفس المحادثة.",
|
||||
"SELECT": "حدد",
|
||||
"SELECT_ALL": "تحديد جميع الوكلاء",
|
||||
"SELECTED_COUNT": "تم تحديد %{selected} من أصل %{total} وكيل.",
|
||||
"SELECTED_COUNT": "تم تحديد {selected} من أصل {total} وكيل.",
|
||||
"BUTTON_TEXT": "إضافة وكلاء",
|
||||
"AGENT_VALIDATION_ERROR": "اختر وكيل واحد على الأقل."
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"MODAL": {
|
||||
"TITLE": "قوالب الواتساب",
|
||||
"SUBTITLE": "حدد القالب الذي تريد إرساله",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "معالجة %{templateName}"
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "معالجة {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "المتغيرات",
|
||||
"VARIABLE_PLACEHOLDER": "أدخل قيمة %{variable}",
|
||||
"VARIABLE_PLACEHOLDER": "أدخل قيمة {variable}",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"SEND_MESSAGE_LABEL": "إرسال الرسالة",
|
||||
"FORM_ERROR_MESSAGE": "يرجى ملء جميع المتغيرات قبل الإرسال"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure want to delete - %{attributeName}",
|
||||
"TITLE": "Are you sure want to delete - {attributeName}",
|
||||
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
||||
"MESSAGE": "Deleting will remove the custom attribute",
|
||||
"YES": "Изтриване ",
|
||||
|
||||
@@ -23,52 +23,52 @@
|
||||
},
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} created a new automation rule (#%{id})",
|
||||
"EDIT": "%{agentName} updated an automation rule (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
|
||||
"ADD": "{agentName} created a new automation rule (#{id})",
|
||||
"EDIT": "{agentName} updated an automation rule (#{id})",
|
||||
"DELETE": "{agentName} deleted an automation rule (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
|
||||
"ADD": "{agentName} invited {invitee} to the account as an {role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} changed their %{attributes} to %{values}",
|
||||
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
|
||||
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
|
||||
"SELF": "{agentName} changed their {attributes} to {values}",
|
||||
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
|
||||
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} created a new inbox (#%{id})",
|
||||
"EDIT": "%{agentName} updated an inbox (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an inbox (#%{id})"
|
||||
"ADD": "{agentName} created a new inbox (#{id})",
|
||||
"EDIT": "{agentName} updated an inbox (#{id})",
|
||||
"DELETE": "{agentName} deleted an inbox (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} created a new webhook (#%{id})",
|
||||
"EDIT": "%{agentName} updated a webhook (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a webhook (#%{id})"
|
||||
"ADD": "{agentName} created a new webhook (#{id})",
|
||||
"EDIT": "{agentName} updated a webhook (#{id})",
|
||||
"DELETE": "{agentName} deleted a webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} signed in",
|
||||
"SIGN_OUT": "%{agentName} signed out"
|
||||
"SIGN_IN": "{agentName} signed in",
|
||||
"SIGN_OUT": "{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} created a new team (#%{id})",
|
||||
"EDIT": "%{agentName} updated a team (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a team (#%{id})"
|
||||
"ADD": "{agentName} created a new team (#{id})",
|
||||
"EDIT": "{agentName} updated a team (#{id})",
|
||||
"DELETE": "{agentName} deleted a team (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} created a new macro (#%{id})",
|
||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||
"ADD": "{agentName} created a new macro (#{id})",
|
||||
"EDIT": "{agentName} updated a macro (#{id})",
|
||||
"DELETE": "{agentName} deleted a macro (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||
"ADD": "{agentName} added {user} to the team(#{team_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Изберете агент",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"YES": "Yes",
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Резюме",
|
||||
"DELETE_WARNING": "Контакт от <strong>%{primaryContactName}</strong> ще бъде изтрит.",
|
||||
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{primaryContactName}</strong> ще бъдат копирани в <strong>%{parentContactName}</strong>."
|
||||
"DELETE_WARNING": "Контакт от <strong>{primaryContactName}</strong> ще бъде изтрит.",
|
||||
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>{primaryContactName}</strong> ще бъдат копирани в <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ГРЕШКА"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"SELECT_PLACEHOLDER": "Нито един",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"NO_RESULTS": "Няма намерени резултати",
|
||||
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
|
||||
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
}
|
||||
},
|
||||
@@ -122,15 +122,15 @@
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"",
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
"FAILED": "Couldn't assign agent. Please try again."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned label #%{labelName} to conversation id %{conversationId}",
|
||||
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign label. Please try again."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}",
|
||||
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign team. Please try again."
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"TITLE": "Hey 👋, Welcome to {installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of {installationName}. Here are a few things you can do in {installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
@@ -317,10 +317,10 @@
|
||||
"SIDEBAR_TITLE": "Conversation participants",
|
||||
"NO_RECORDS_FOUND": "Няма намерени резултати",
|
||||
"ADD_PARTICIPANTS": "Select participants",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
|
||||
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
|
||||
"WATCH_CONVERSATION": "Join conversation",
|
||||
"YOU_ARE_WATCHING": "You are participating",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"LEARN_MORE": "Learn more",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"ARTICLE_SEARCH_RESULT": {
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"SEARCH_RESULTS": "Search results for %{query}",
|
||||
"SEARCH_RESULTS": "Search results for {query}",
|
||||
"EMPTY_TEXT": "Search for articles to insert into replies.",
|
||||
"SEARCH_LOADER": "Searching...",
|
||||
"INSERT_ARTICLE": "Insert",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"STATUS": {
|
||||
"ENABLED": "Включен",
|
||||
@@ -56,7 +56,7 @@
|
||||
"BUTTON_TEXT": "Прекъсване"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with {installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Потвърди изтриването",
|
||||
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
|
||||
"MESSAGE": "Are you sure to delete the webhook? ({webhookURL})",
|
||||
"YES": "Да, изтрий ",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"OPEN_AI": {
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"WITH_AI": " %{option} with AI ",
|
||||
"WITH_AI": " {option} with AI ",
|
||||
"OPTIONS": {
|
||||
"REPLY_SUGGESTION": "Reply Suggestion",
|
||||
"SUMMARIZE": "Summarize",
|
||||
@@ -216,7 +216,7 @@
|
||||
"CONFIRM_YES": "Yes, delete it",
|
||||
"CONFIRM_NO": "No, keep it",
|
||||
"TITLE": "Потвърди изтриването",
|
||||
"MESSAGE": "Are you sure to delete the app - %{appName}?",
|
||||
"MESSAGE": "Are you sure to delete the app - {appName}?",
|
||||
"API_SUCCESS": "Dashboard app deleted successfully",
|
||||
"API_ERROR": "We couldn't delete the app. Please try again later"
|
||||
}
|
||||
@@ -235,7 +235,7 @@
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
"LINK_TITLE": "Conversation (#{conversationId}) with {name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
@@ -294,7 +294,7 @@
|
||||
"PRIORITY": "Priority",
|
||||
"ASSIGNEE": "Assignee",
|
||||
"LABELS": "Етикети",
|
||||
"CREATED_AT": "Created at %{createdAt}"
|
||||
"CREATED_AT": "Created at {createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Unlink",
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"NAME": "Customer waiting time",
|
||||
"TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)"
|
||||
"TOOLTIP_TEXT": "Waiting time is {metricValue} (based on {conversationCount} replies)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
@@ -167,13 +167,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -234,13 +234,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -301,13 +301,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -368,13 +368,13 @@
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -480,8 +480,8 @@
|
||||
"CONVERSATION_HEATMAP": {
|
||||
"HEADER": "Conversation Traffic",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "%{count} conversation",
|
||||
"CONVERSATIONS": "%{count} conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"MESSAGES": "Messages"
|
||||
},
|
||||
"SEARCHING_DATA": "Searching",
|
||||
"EMPTY_STATE": "No %{item} found for query '%{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
"TITLE": "Billing",
|
||||
"CURRENT_PLAN": {
|
||||
"TITLE": "Current Plan",
|
||||
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
|
||||
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
|
||||
},
|
||||
"MANAGE_SUBSCRIPTION": {
|
||||
"TITLE": "Manage your subscription",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Add agents to team",
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Update agents in team",
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. All the added agents will be notified when a conversation is assigned to this team."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -77,14 +77,14 @@
|
||||
"ADD_AGENTS": "Adding Agents to your Team...",
|
||||
"SELECT": "select",
|
||||
"SELECT_ALL": "select all agents",
|
||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected."
|
||||
"SELECTED_COUNT": "{selected} out of {total} agents selected."
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"TITLE": "Add agents to team - {teamName}",
|
||||
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation.",
|
||||
"SELECT": "select",
|
||||
"SELECT_ALL": "select all agents",
|
||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
|
||||
"SELECTED_COUNT": "{selected} out of {total} agents selected.",
|
||||
"BUTTON_TEXT": "Add agents",
|
||||
"AGENT_VALIDATION_ERROR": "Select at least one agent."
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"ERROR_MESSAGE": "No s'ha pogut suprimir l'atribut personalitzat. Torna-ho a provar."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Estàs segur que vols suprimir - %{attributeName}",
|
||||
"TITLE": "Estàs segur que vols suprimir - {attributeName}",
|
||||
"PLACE_HOLDER": "Escriu {attributeName} per confirmar",
|
||||
"MESSAGE": "En suprimir, s'eliminarà l'atribut personalitzat",
|
||||
"YES": "Suprimeix ",
|
||||
|
||||
@@ -23,52 +23,52 @@
|
||||
},
|
||||
"DEFAULT_USER": "Sistema",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} ha creat una nova regla d'automatització (#%{id})",
|
||||
"EDIT": "%{agentName} ha actualitzat una regla d'automatització (#%{id})",
|
||||
"DELETE": "%{agentName} ha suprimit una regla d'automatització (#%{id})"
|
||||
"ADD": "{agentName} ha creat una nova regla d'automatització (#{id})",
|
||||
"EDIT": "{agentName} ha actualitzat una regla d'automatització (#{id})",
|
||||
"DELETE": "{agentName} ha suprimit una regla d'automatització (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} ha convidat %{invitee} al compte com a %{role}",
|
||||
"ADD": "{agentName} ha convidat {invitee} al compte com a {role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} ha canviat els seus %{attributes} a %{values}",
|
||||
"OTHER": "%{agentName} ha canviat %{attributes} de %{user} a %{values}",
|
||||
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
|
||||
"SELF": "{agentName} ha canviat els seus {attributes} a {values}",
|
||||
"OTHER": "{agentName} ha canviat {attributes} de {user} a {values}",
|
||||
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} ha creat una safata d'entrada nova (#%{id})",
|
||||
"EDIT": "%{agentName} ha actualitzat una safata d'entrada (#%{id})",
|
||||
"DELETE": "%{agentName} ha suprimit una safata d'entrada (#%{id})"
|
||||
"ADD": "{agentName} ha creat una safata d'entrada nova (#{id})",
|
||||
"EDIT": "{agentName} ha actualitzat una safata d'entrada (#{id})",
|
||||
"DELETE": "{agentName} ha suprimit una safata d'entrada (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} ha creat un nou webhook (#%{id})",
|
||||
"EDIT": "%{agentName} ha actualitzat un webhook (#%{id})",
|
||||
"DELETE": "%{agentName} ha suprimit un webhook (#%{id})"
|
||||
"ADD": "{agentName} ha creat un nou webhook (#{id})",
|
||||
"EDIT": "{agentName} ha actualitzat un webhook (#{id})",
|
||||
"DELETE": "{agentName} ha suprimit un webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} ha iniciat la sessió",
|
||||
"SIGN_OUT": "%{agentName} ha tancat la sessió"
|
||||
"SIGN_IN": "{agentName} ha iniciat la sessió",
|
||||
"SIGN_OUT": "{agentName} ha tancat la sessió"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} ha creat un equip nou (#%{id})",
|
||||
"EDIT": "%{agentName} ha actualitzat un equip (#%{id})",
|
||||
"DELETE": "%{agentName} ha suprimit un equip (#%{id})"
|
||||
"ADD": "{agentName} ha creat un equip nou (#{id})",
|
||||
"EDIT": "{agentName} ha actualitzat un equip (#{id})",
|
||||
"DELETE": "{agentName} ha suprimit un equip (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} ha creat una macro nova (#%{id})",
|
||||
"EDIT": "%{agentName} ha actualitzat una macro (#%{id})",
|
||||
"DELETE": "%{agentName} ha suprimit una macro (#%{id})"
|
||||
"ADD": "{agentName} ha creat una macro nova (#{id})",
|
||||
"EDIT": "{agentName} ha actualitzat una macro (#{id})",
|
||||
"DELETE": "{agentName} ha suprimit una macro (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} ha afegit %{user} a la safata d'entrada(#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} ha eliminat %{user} de la safata d'entrada(#%{inbox_id})"
|
||||
"ADD": "{agentName} ha afegit {user} a la safata d'entrada(#{inbox_id})",
|
||||
"REMOVE": "{agentName} ha eliminat {user} de la safata d'entrada(#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} ha afegit %{user} a l'equip (#%{team_id})",
|
||||
"REMOVE": "%{agentName} ha eliminat %{user} de l'equip (#%{team_id})"
|
||||
"ADD": "{agentName} ha afegit {user} a l'equip (#{team_id})",
|
||||
"REMOVE": "{agentName} ha eliminat {user} de l'equip (#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} ha actualitzat la configuració del compte (#%{id})"
|
||||
"EDIT": "{agentName} ha actualitzat la configuració del compte (#{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} converses seleccionades",
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} converses seleccionades",
|
||||
"AGENT_SELECT_LABEL": "Seleccionar Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Estas segur d'assignar %{conversationCount} %{conversationLabel} a",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Estàs segur de desassignar %{conversationCount} %{conversationLabel}?",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Estas segur d'assignar {conversationCount} {conversationLabel} a",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Estàs segur de desassignar {conversationCount} {conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Torna",
|
||||
"ASSIGN_LABEL": "Assignar",
|
||||
"YES": "Si",
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Resum",
|
||||
"DELETE_WARNING": "El contacte de <strong>%{primaryContactName}</strong> es suprimirà.",
|
||||
"ATTRIBUTE_WARNING": "Les dades de contacte de <strong>%{primaryContactName}</strong> es copiaran a <strong>%{parentContactName}</strong>."
|
||||
"DELETE_WARNING": "El contacte de <strong>{primaryContactName}</strong> es suprimirà.",
|
||||
"ATTRIBUTE_WARNING": "Les dades de contacte de <strong>{primaryContactName}</strong> es copiaran a <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"SELECT_PLACEHOLDER": "Ningú",
|
||||
"INPUT_PLACEHOLDER": "Selecciona la prioritat",
|
||||
"NO_RESULTS": "No s'ha trobat agents",
|
||||
"SUCCESSFUL": "S'ha canviat la prioritat de l'identificador de conversa %{conversationId} a %{priority}",
|
||||
"SUCCESSFUL": "S'ha canviat la prioritat de l'identificador de conversa {conversationId} a {priority}",
|
||||
"FAILED": "No s'ha pogut canviar la prioritat. Si us plau, torna-ho a provar."
|
||||
}
|
||||
},
|
||||
@@ -122,15 +122,15 @@
|
||||
"ASSIGN_TEAM": "Assigna un equip",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Id de conversa %{conversationId} assignat a \"%{agentName}\"",
|
||||
"SUCCESFUL": "Id de conversa {conversationId} assignat a \"{agentName}\"",
|
||||
"FAILED": "No s'ha pogut assignar l'agent. Torna-ho a provar."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "S'ha assignat l'etiqueta #%{labelName} a l'id de conversa %{conversationId}",
|
||||
"SUCCESFUL": "S'ha assignat l'etiqueta #{labelName} a l'id de conversa {conversationId}",
|
||||
"FAILED": "No s'ha pogut assignar l'etiqueta. Torna-ho a provar."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "S'ha assignat l'equip \"%{team}\" a l'id de conversa %{conversationId}",
|
||||
"SUCCESFUL": "S'ha assignat l'equip \"{team}\" a l'id de conversa {conversationId}",
|
||||
"FAILED": "No s'ha pogut assignar l'equip. Torna-ho a provar."
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hola 👋, Benvingut a %{installationName}!",
|
||||
"DESCRIPTION": "Gràcies per registrar-te. Volem que treguis el màxim profit de %{installationName}. Aquí teniu algunes coses que pots fer a %{installationName} perquè l'experiència sigui agradable.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"TITLE": "Hola 👋, Benvingut a {installationName}!",
|
||||
"DESCRIPTION": "Gràcies per registrar-te. Volem que treguis el màxim profit de {installationName}. Aquí teniu algunes coses que pots fer a {installationName} perquè l'experiència sigui agradable.",
|
||||
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
|
||||
"READ_LATEST_UPDATES": "Llegiu les nostres últimes actualitzacions",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Totes les teves converses en un sol lloc",
|
||||
@@ -317,10 +317,10 @@
|
||||
"SIDEBAR_TITLE": "Participants de la conversa",
|
||||
"NO_RECORDS_FOUND": "No s'ha trobat agents",
|
||||
"ADD_PARTICIPANTS": "Selecciona els participants",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+%{count} més",
|
||||
"REMANING_PARTICIPANT_TEXT": "+%{count} més",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "%{count} persones estan participant.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "%{count} persona està participant.",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} més",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} més",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} persones estan participant.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} persona està participant.",
|
||||
"NO_PARTICIPANTS_TEXT": "No hi participa ningú!",
|
||||
"WATCH_CONVERSATION": "Uneix-te a la conversa",
|
||||
"YOU_ARE_WATCHING": "Estàs participant",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Ara podeu rebre correus electrònics al vostre domini personalitzat."
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "L'actualització %{latestChatwootVersion} per Chatwoot està disponible. Si us plau, actualitza l'instancia.",
|
||||
"UPDATE_CHATWOOT": "L'actualització {latestChatwootVersion} per Chatwoot està disponible. Si us plau, actualitza l'instancia.",
|
||||
"LEARN_MORE": "Aprèn més",
|
||||
"PAYMENT_PENDING": "El teu pagament està pendent. Actualitzeu la vostra informació de pagament per continuar utilitzant Chatwoot",
|
||||
"LIMITS_UPGRADE": "El teu compte ha superat els límits d'ús, actualitza el pla per continuar utilitzant Chatwoot",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"ARTICLE_SEARCH_RESULT": {
|
||||
"UNCATEGORIZED": "Sense categoria",
|
||||
"SEARCH_RESULTS": "Resultats de la cerca per a %{query}",
|
||||
"SEARCH_RESULTS": "Resultats de la cerca per a {query}",
|
||||
"EMPTY_TEXT": "Cerca articles per inserir a les respostes.",
|
||||
"SEARCH_LOADER": "S'està cercant...",
|
||||
"INSERT_ARTICLE": "Insereix",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "S'estan obtenint integracions",
|
||||
"NO_HOOK_CONFIGURED": "No hi ha integracions %{integrationId} configurades en aquest compte.",
|
||||
"NO_HOOK_CONFIGURED": "No hi ha integracions {integrationId} configurades en aquest compte.",
|
||||
"HEADER": "Aplicacions",
|
||||
"STATUS": {
|
||||
"ENABLED": "Habilita",
|
||||
@@ -56,7 +56,7 @@
|
||||
"BUTTON_TEXT": "Desconnecta"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "Dialogflow és una plataforma de comprensió del llenguatge natural que facilita el disseny i la integració d'una interfície d'usuari conversacional a la teva aplicació mòbil, aplicació web, dispositiu, bot, sistema interactiu de resposta de veu, etc. <br /> <br /> La integració de Dialogflow amb %{installationName} us permet configurar un bot de Dialogflow amb les teves safates d'entrada, la qual cosa permet al bot gestionar les consultes inicialment i lliurar-les a un agent quan sigui necessari. Dialogflow es pot utilitzar per qualificar els clients potencials, reduir la càrrega de treball dels agents proporcionant preguntes freqüents, etc. <br /> <br /> Per afegir Dialogflow, has de crear un compte de servei a la consola del vostre projecte de Google i compartir les credencials. Consulta els documents de Dialogflow per obtenir més informació."
|
||||
"DIALOGFLOW": "Dialogflow és una plataforma de comprensió del llenguatge natural que facilita el disseny i la integració d'una interfície d'usuari conversacional a la teva aplicació mòbil, aplicació web, dispositiu, bot, sistema interactiu de resposta de veu, etc. <br /> <br /> La integració de Dialogflow amb {installationName} us permet configurar un bot de Dialogflow amb les teves safates d'entrada, la qual cosa permet al bot gestionar les consultes inicialment i lliurar-les a un agent quan sigui necessari. Dialogflow es pot utilitzar per qualificar els clients potencials, reduir la càrrega de treball dels agents proporcionant preguntes freqüents, etc. <br /> <br /> Per afegir Dialogflow, has de crear un compte de servei a la consola del vostre projecte de Google i compartir les credencials. Consulta els documents de Dialogflow per obtenir més informació."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "N'estàs segur que suprimiu el webhook? (%{webhookURL})",
|
||||
"MESSAGE": "N'estàs segur que suprimiu el webhook? ({webhookURL})",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, mantén-la"
|
||||
}
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Com utilitzar la integració de Slack?",
|
||||
"BODY": "Amb aquesta integració, totes les teves converses entrants se sincronitzaran amb el canal ***%{selectedChannelName}*** del teu espai de treball de Slack. Podeu gestionar totes les converses dels vostres clients directament des del canal i no perdreu cap missatge.\n\nAquestes són les principals característiques de la integració:\n\n**Respondre a converses des de Slack:** Per respondre a una conversa al ***%{selectedChannelName}*** canal de Slack, només has d'escriure el teu missatge i enviar-lo com a fil. Això crearà una resposta al client mitjançant Chatwoot. Així de senzill!\n\n **Crear notes privades:** si vols crear notes privades en lloc de respostes, comença el teu missatge amb ***`nota:`***. Això garanteix que el vostre missatge es mantingui privat i no serà visible per al client.\n\n**Associar un perfil d'agent:** si la persona que va respondre a Slack té un perfil d'agent a Chatwoot amb el mateix correu electrònic, les respostes s'associaran amb aquest perfil d'agent automàticament. Això vol dir que podeu fer un seguiment fàcilment qui va dir què i quan. D'altra banda, quan el contestador no té un perfil d'agent associat, les respostes apareixeran des del perfil del bot al client.",
|
||||
"BODY": "Amb aquesta integració, totes les teves converses entrants se sincronitzaran amb el canal ***{selectedChannelName}*** del teu espai de treball de Slack. Podeu gestionar totes les converses dels vostres clients directament des del canal i no perdreu cap missatge.\n\nAquestes són les principals característiques de la integració:\n\n**Respondre a converses des de Slack:** Per respondre a una conversa al ***{selectedChannelName}*** canal de Slack, només has d'escriure el teu missatge i enviar-lo com a fil. Això crearà una resposta al client mitjançant Chatwoot. Així de senzill!\n\n **Crear notes privades:** si vols crear notes privades en lloc de respostes, comença el teu missatge amb ***`nota:`***. Això garanteix que el vostre missatge es mantingui privat i no serà visible per al client.\n\n**Associar un perfil d'agent:** si la persona que va respondre a Slack té un perfil d'agent a Chatwoot amb el mateix correu electrònic, les respostes s'associaran amb aquest perfil d'agent automàticament. Això vol dir que podeu fer un seguiment fàcilment qui va dir què i quan. D'altra banda, quan el contestador no té un perfil d'agent associat, les respostes apareixeran des del perfil del bot al client.",
|
||||
"SELECTED": "seleccionat"
|
||||
},
|
||||
"SELECT_CHANNEL": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"OPEN_AI": {
|
||||
"AI_ASSIST": "Assistència IA",
|
||||
"WITH_AI": " %{option} amb IA ",
|
||||
"WITH_AI": " {option} amb IA ",
|
||||
"OPTIONS": {
|
||||
"REPLY_SUGGESTION": "Suggeriment de resposta",
|
||||
"SUMMARIZE": "Resumir",
|
||||
@@ -216,7 +216,7 @@
|
||||
"CONFIRM_YES": "Sí, esborra-ho",
|
||||
"CONFIRM_NO": "No, mantén-la",
|
||||
"TITLE": "Confirma la supressió",
|
||||
"MESSAGE": "N'estàs segur que vols suprimir l'aplicació - %{appName}?",
|
||||
"MESSAGE": "N'estàs segur que vols suprimir l'aplicació - {appName}?",
|
||||
"API_SUCCESS": "L'aplicació del tauler de control s'ha esborrat correctament",
|
||||
"API_ERROR": "No hem pogut esborrar l'aplicació. Intenta-ho més tard"
|
||||
}
|
||||
@@ -235,7 +235,7 @@
|
||||
"ERROR": "S'ha produït un error en obtenir les issues en Linear, torna-ho a provar",
|
||||
"LINK_SUCCESS": "S'ha enllaçat la issue correctament",
|
||||
"LINK_ERROR": "S'ha produït un error en enllaçar la issue, torna-ho a provar",
|
||||
"LINK_TITLE": "Conversa (#%{conversationId}) amb %{name}"
|
||||
"LINK_TITLE": "Conversa (#{conversationId}) amb {name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Crear/enllaçar una issue en Linear",
|
||||
@@ -294,7 +294,7 @@
|
||||
"PRIORITY": "Prioritat",
|
||||
"ASSIGNEE": "Cessionari",
|
||||
"LABELS": "Etiquetes",
|
||||
"CREATED_AT": "Creat a %{createdAt}"
|
||||
"CREATED_AT": "Creat a {createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Desenllaça",
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"NAME": "Primer Temps de Resposta",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"NAME": "Temps d'espera del client",
|
||||
"TOOLTIP_TEXT": "El temps d'espera és %{metricValue} (basat en %{conversationCount} respostes)"
|
||||
"TOOLTIP_TEXT": "El temps d'espera és {metricValue} (basat en {conversationCount} respostes)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
@@ -167,13 +167,13 @@
|
||||
"NAME": "Primer Temps de Resposta",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
@@ -234,13 +234,13 @@
|
||||
"NAME": "Primer Temps de Resposta",
|
||||
"DESC": "(Mitjana)",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
@@ -301,13 +301,13 @@
|
||||
"NAME": "Primer Temps de Resposta",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
@@ -368,13 +368,13 @@
|
||||
"NAME": "Primer Temps de Resposta",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de primera resposta (FRT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )",
|
||||
"INFO_TEXT": "Nombre total de converses utilitzades per al càlcul:",
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és %{metricValue} (basat en %{conversationCount} converses)"
|
||||
"TOOLTIP_TEXT": "El temps de resolució (RT) és {metricValue} (basat en {conversationCount} converses)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
@@ -480,8 +480,8 @@
|
||||
"CONVERSATION_HEATMAP": {
|
||||
"HEADER": "Trànsit de conversa",
|
||||
"NO_CONVERSATIONS": "Sense converses",
|
||||
"CONVERSATION": "%{count} conversa",
|
||||
"CONVERSATIONS": "%{count} converses",
|
||||
"CONVERSATION": "{count} conversa",
|
||||
"CONVERSATIONS": "{count} converses",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"MESSAGES": "Missatges"
|
||||
},
|
||||
"SEARCHING_DATA": "S'està cercant",
|
||||
"EMPTY_STATE": "No s'ha trobat cap %{item} per a la consulta '%{query}'",
|
||||
"EMPTY_STATE_FULL": "No s'han trobat resultats per a la consulta '%{query}'",
|
||||
"EMPTY_STATE": "No s'ha trobat cap {item} per a la consulta '{query}'",
|
||||
"EMPTY_STATE_FULL": "No s'han trobat resultats per a la consulta '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ centrar",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
"TITLE": "Facturació",
|
||||
"CURRENT_PLAN": {
|
||||
"TITLE": "Pla actual",
|
||||
"PLAN_NOTE": "Actualment estàs subscrit al pla **%{plan}** amb **%{quantity}** llicències"
|
||||
"PLAN_NOTE": "Actualment estàs subscrit al pla **{plan}** amb **{quantity}** llicències"
|
||||
},
|
||||
"MANAGE_SUBSCRIPTION": {
|
||||
"TITLE": "Gestiona la teva subscripció",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Afegir agents a l'equip",
|
||||
"TITLE": "Afegeix agents a l'equip - %{teamName}",
|
||||
"TITLE": "Afegeix agents a l'equip - {teamName}",
|
||||
"DESC": "Afegeix agents al teu equip acabat de crear. Això et permet col·laborar com a equip en converses, rebre notificacions sobre esdeveniments nous a la mateixa conversa."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Actualitza els agents de l'equip",
|
||||
"TITLE": "Afegeix agents a l'equip - %{teamName}",
|
||||
"TITLE": "Afegeix agents a l'equip - {teamName}",
|
||||
"DESC": "Afegeix agents al teu equip acabat de crear. Tots els agents afegits rebran una notificació quan s'assigni una conversa a aquest equip."
|
||||
},
|
||||
"WIZARD": [
|
||||
@@ -77,14 +77,14 @@
|
||||
"ADD_AGENTS": "S'estan afegint agents al teu equip...",
|
||||
"SELECT": "selecciona",
|
||||
"SELECT_ALL": "selecciona tots els agents",
|
||||
"SELECTED_COUNT": "%{selected} de %{total} agents seleccionats."
|
||||
"SELECTED_COUNT": "{selected} de {total} agents seleccionats."
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegeix agents a l'equip - %{teamName}",
|
||||
"TITLE": "Afegeix agents a l'equip - {teamName}",
|
||||
"DESC": "Afegeix agents al teu equip acabat de crear. Això et permet col·laborar com a equip en converses, rebre notificacions sobre esdeveniments nous a la mateixa conversa.",
|
||||
"SELECT": "selecciona",
|
||||
"SELECT_ALL": "selecciona tots els agents",
|
||||
"SELECTED_COUNT": "%{selected} de %{total} agents seleccionats.",
|
||||
"SELECTED_COUNT": "{selected} de {total} agents seleccionats.",
|
||||
"BUTTON_TEXT": "Afegir agents",
|
||||
"AGENT_VALIDATION_ERROR": "Selecciona almenys un agent."
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"MODAL": {
|
||||
"TITLE": "Plantilles de Whatsapp",
|
||||
"SUBTITLE": "Selecciona la plantilla de whatsapp que vols enviar",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Procés %{templateName}"
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Procés {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Cerca plantilles",
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Introdueix el valor %{variable}",
|
||||
"VARIABLE_PLACEHOLDER": "Introdueix el valor {variable}",
|
||||
"GO_BACK_LABEL": "Torna enrere",
|
||||
"SEND_MESSAGE_LABEL": "Envia missatge",
|
||||
"FORM_ERROR_MESSAGE": "Omple totes les variables abans d'enviar-les"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure want to delete - %{attributeName}",
|
||||
"TITLE": "Are you sure want to delete - {attributeName}",
|
||||
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
||||
"MESSAGE": "Deleting will remove the custom attribute",
|
||||
"YES": "Vymazat ",
|
||||
|
||||
@@ -23,52 +23,52 @@
|
||||
},
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} created a new automation rule (#%{id})",
|
||||
"EDIT": "%{agentName} updated an automation rule (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
|
||||
"ADD": "{agentName} created a new automation rule (#{id})",
|
||||
"EDIT": "{agentName} updated an automation rule (#{id})",
|
||||
"DELETE": "{agentName} deleted an automation rule (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
|
||||
"ADD": "{agentName} invited {invitee} to the account as an {role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} changed their %{attributes} to %{values}",
|
||||
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}",
|
||||
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
|
||||
"SELF": "{agentName} changed their {attributes} to {values}",
|
||||
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
|
||||
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} created a new inbox (#%{id})",
|
||||
"EDIT": "%{agentName} updated an inbox (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an inbox (#%{id})"
|
||||
"ADD": "{agentName} created a new inbox (#{id})",
|
||||
"EDIT": "{agentName} updated an inbox (#{id})",
|
||||
"DELETE": "{agentName} deleted an inbox (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} created a new webhook (#%{id})",
|
||||
"EDIT": "%{agentName} updated a webhook (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a webhook (#%{id})"
|
||||
"ADD": "{agentName} created a new webhook (#{id})",
|
||||
"EDIT": "{agentName} updated a webhook (#{id})",
|
||||
"DELETE": "{agentName} deleted a webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} signed in",
|
||||
"SIGN_OUT": "%{agentName} signed out"
|
||||
"SIGN_IN": "{agentName} signed in",
|
||||
"SIGN_OUT": "{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} created a new team (#%{id})",
|
||||
"EDIT": "%{agentName} updated a team (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a team (#%{id})"
|
||||
"ADD": "{agentName} created a new team (#{id})",
|
||||
"EDIT": "{agentName} updated a team (#{id})",
|
||||
"DELETE": "{agentName} deleted a team (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} created a new macro (#%{id})",
|
||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||
"ADD": "{agentName} created a new macro (#{id})",
|
||||
"EDIT": "{agentName} updated a macro (#{id})",
|
||||
"DELETE": "{agentName} deleted a macro (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||
"ADD": "{agentName} added {user} to the team(#{team_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Vybrat agenta",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Přiřadit",
|
||||
"YES": "Ano",
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
|
||||
"DELETE_WARNING": "Contact of <strong>{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"SELECT_PLACEHOLDER": "Nic",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"NO_RESULTS": "Žádné výsledky",
|
||||
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
|
||||
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
}
|
||||
},
|
||||
@@ -122,15 +122,15 @@
|
||||
"ASSIGN_TEAM": "Přiřadit tým",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Konverzace id %{conversationId} přiřazena \"%{agentName}\"",
|
||||
"SUCCESFUL": "Konverzace id {conversationId} přiřazena \"{agentName}\"",
|
||||
"FAILED": "Nelze přiřadit agenta. Zkuste to prosím znovu."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Přiřazený štítek #%{labelName} ke konverzaci id %{conversationId}",
|
||||
"SUCCESFUL": "Přiřazený štítek #{labelName} ke konverzaci id {conversationId}",
|
||||
"FAILED": "Nelze přiřadit štítek. Zkuste to prosím znovu."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Přiřazený tým #%{team} ke konverzaci id %{conversationId}",
|
||||
"SUCCESFUL": "Přiřazený tým #{team} ke konverzaci id {conversationId}",
|
||||
"FAILED": "Nelze přiřadit tým. Zkuste to prosím znovu."
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Ahoj, 👋, Vítejte na %{installationName}!",
|
||||
"DESCRIPTION": "Děkujeme za registraci. Chceme, abyste získali maximum z %{installationName}. Zde je několik věcí, které můžete v %{installationName} udělat, abyste udělali zážitek příjemný.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"TITLE": "Ahoj, 👋, Vítejte na {installationName}!",
|
||||
"DESCRIPTION": "Děkujeme za registraci. Chceme, abyste získali maximum z {installationName}. Zde je několik věcí, které můžete v {installationName} udělat, abyste udělali zážitek příjemný.",
|
||||
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
|
||||
"READ_LATEST_UPDATES": "Přečtěte si nejnovější aktualizace",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Všechny vaše konverzace na jednom místě",
|
||||
@@ -317,10 +317,10 @@
|
||||
"SIDEBAR_TITLE": "Conversation participants",
|
||||
"NO_RECORDS_FOUND": "Žádné výsledky",
|
||||
"ADD_PARTICIPANTS": "Select participants",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
|
||||
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
|
||||
"WATCH_CONVERSATION": "Join conversation",
|
||||
"YOU_ARE_WATCHING": "You are participating",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user