Compare commits

...
Author SHA1 Message Date
Shivam Mishra 68ed5a4b4f chore: source account id directly 2024-10-08 18:52:34 +05:30
Shivam Mishra 7ecb993d06 chore: force init account 2024-10-08 18:49:26 +05:30
Shivam Mishra c00a157c03 chore: more log 2024-10-08 18:45:08 +05:30
Shivam Mishra 3750689bee chore: debug debug 2024-10-08 18:43:37 +05:30
Shivam Mishra 0511bf4ba6 chore: more debug 2024-10-08 18:34:06 +05:30
Shivam Mishra 9f50638a40 chore: more logs 2024-10-08 18:30:59 +05:30
Shivam Mishra f5ac272e3b chore: logs 2024-10-08 18:18:49 +05:30
Shivam Mishra 2ba7318817 chore: more try 2024-10-08 18:14:58 +05:30
Shivam Mishra 412303d1cd chore: try i18n 2024-10-08 15:20:35 +05:30
Shivam Mishra 47b9399208 feat: setup fallback 2024-10-08 14:01:57 +05:30
Shivam Mishra 0830ad78b6 chore: try 2024-10-08 13:53:58 +05:30
Shivam Mishra 0b8df762f9 chore: more debug 2024-10-08 12:38:03 +05:30
Shivam Mishra f24c87f9b2 chore: try 2024-10-08 12:28:48 +05:30
Shivam Mishra f62a4170b6 chore: debug 2024-10-08 12:20:14 +05:30
Shivam Mishra 2f090dbc61 Revert "chore: don't use async"
This reverts commit 9101e1e221.
2024-10-08 11:54:02 +05:30
Shivam Mishra ec17682d7a chore: move to created 2024-10-08 11:53:55 +05:30
Shivam Mishra 7861ce8c47 chore: revert iframe helper 2024-10-08 11:42:09 +05:30
Shivam Mishra e5de41fea4 chore: revert console.logs 2024-10-08 11:41:40 +05:30
Shivam Mishra adb0e94cbd chore: remove logs 2024-10-08 11:40:53 +05:30
Shivam Mishra 42fb083491 chore: debug 2024-10-08 11:31:33 +05:30
Shivam Mishra 9101e1e221 chore: don't use async 2024-10-08 11:18:13 +05:30
Shivam Mishra 91bac16c29 chore: add more error log 2024-10-08 11:00:21 +05:30
Shivam Mishra 97699f3087 chore: log errors 2024-10-08 10:59:42 +05:30
Shivam Mishra 1242cfd704 chore: add logs for iframhelper 2024-10-08 10:57:13 +05:30
Shivam Mishra 6963c668bf chore: add console logs 2024-10-08 10:49:56 +05:30
Shivam MishraandGitHub a8c12ffb25 chore: update interpolation syntax for i18n files (#10198)
There were two warnings showing up on new teams page

1. `errorMessage` prop was getting invalid value, this was because were
short circuting the error message using `&&`, fixed it by using ternary
operator
2. `vue-i18n` has deprecated [rails
syntax](https://vue-i18n.intlify.dev/guide/essentials/syntax#rails-i18n-format),
fixed that warning by removing `%` before `{}` for interpolation

> Note: the `vue-i18n` deprecation needs to be handled, but we can do it
later
2024-10-07 11:12:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
97d4ce65c4 chore(deps): bump braces from 3.0.2 to 3.0.3 (#10238)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to
3.0.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="https://github.com/micromatch/braces/commit/88f1429a0f47e1dd3813de35211fc97ffda27f9e"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="https://github.com/micromatch/braces/commit/415d660c3002d1ab7e63dbf490c9851da80596ff"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/190510f79db1adf21d92798b0bb6fccc1f72c9d6"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="https://github.com/micromatch/braces/commit/716eb9f12d820b145a831ad678618731927e8856"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="https://github.com/micromatch/braces/commit/a5851e57f45c3431a94d83fc565754bc10f5bbc3"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="https://github.com/micromatch/braces/commit/2092bd1fb108d2c59bd62e243b70ad98db961538"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="https://github.com/micromatch/braces/commit/9f5b4cf47329351bcb64287223ffb6ecc9a5e6d3"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/98414f9f1fabe021736e26836d8306d5de747e0d"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="https://github.com/micromatch/braces/commit/665ab5d561c017a38ba7aafd92cc6655b91d8c14"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:12:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ea787fecd8 chore(deps): bump dset from 3.1.3 to 3.1.4 (#10239)
Bumps [dset](https://github.com/lukeed/dset) from 3.1.3 to 3.1.4.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/lukeed/dset/commit/05b1ec0f8cebe3b5d5667285bc7da90f38a7215e"><code>05b1ec0</code></a>
3.1.4</li>
<li><a
href="https://github.com/lukeed/dset/commit/16d6154e085bef01e99f01330e5a421a7f098afa"><code>16d6154</code></a>
fix: prevent <strong>proto</strong> assignment via implicit string</li>
<li>See full diff in <a
href="https://github.com/lukeed/dset/compare/v3.1.3...v3.1.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dset&package-manager=npm_and_yarn&previous-version=3.1.3&new-version=3.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:11:36 -07:00
ee02923ace chore: fix circleci on vite build (#10214)
- Switch to pnpm based build
- Switch circleci from docker to machine to have more memory
- Fix frontend and backend tests

Fixes
https://linear.app/chatwoot/issue/CW-3610/fix-circle-ci-for-vite-build
---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-10-07 15:27:41 +05:30
0677d8763d fix: Update the reply box to handle play, pause callbacks from WaveSurfer (#10223)
- Implemented custom @play, @pause methods to update the state of the recording. Once the recording is finished the button icon changes from stop button to play/pause button.
- Removes the console error undefined hasAudio

Fixes https://linear.app/chatwoot/issue/CW-3609/audio-recorder-issue

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-10-05 00:17:11 -07:00
Sivin VargheseandGitHub 095aaf3de6 fix: Issues with drag and drop attachments to the editor (#10235) 2024-10-05 00:16:42 -07:00
PranavandGitHub b7aea99f44 fix: Add webhook string to the zh_CN to fix missing webhook feature (#10234)
The integrations are displayed based on whether the integration object
is available on the API. Since there are chances where empty objects can
be returned, the check for the presence was done based on the
"integration.name". The name was empty (this should be investigated) in
the zh_CN locale, which led to the feature being disabled for a user.

In this PR, I just added the string which should enable the feature. In
the followups, I will add a better check for the presence of the
integration object.
2024-10-04 17:50:38 -07:00
Sivin VargheseandGitHub d0bf66fe7a feat: Update widget builder to support dark mode (#10230) 2024-10-04 15:53:31 -07:00
Shivam MishraandGitHub 490fc4eaba fix: Remove the usage of asyncComponent in emoji picker (#10232)
This PR has the following fixes

1. preview of article inside the iframe, earlier this didn't work because the iframe didn't get the URL correctly. We fix that by passing the URL is a query instead
2. Emoji picker caused a weird redirect, this was only happening when the chunk was loaded async, this PR changes it to use regular loading instead
2024-10-04 08:10:27 -07:00
Vishnu NarayananandGitHub e6e0f7de5a fix: Add missing 'one' translations for time units in agent-reports (#10228) 2024-10-04 08:08:25 -07:00
d2eda8bd08 fix: Update inconsistent behaviour of create and edit automation rules (#10197)
When Vue 3 is used with options API, any assignment to `this.<something>` is converted to a Proxy before assignment. This is fine as long as we are in the options context, problem arises when we access this in a `composable` any mutations on the object doesn't behave correctly as expected, this PR fixes that by moving the `automation` object inside the composable and using it in the options.

> Another option to fix such an issue is to make the object non-reactive, like done in places where we have `editorView`, but that wasn't viable here

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-10-04 08:06:36 -07:00
55bf05d2d4 fix: Use arrow function to bind methods to class (#10215)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2024-10-04 08:05:34 -07:00
Shivam MishraandGitHub 5b22af6af8 fix: Update input event bindings to use blur events (#10216) 2024-10-04 08:04:56 -07:00
9338bc1391 fix: emit events across the app (#10227)
This PR makes the following changes

1. Update v-model bindings for components using the old `value` prop and `input` event method
2. Remove components that were not used anywhere

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-10-04 08:03:41 -07:00
Shivam MishraandGitHub 88a16b8e96 fix: submit pre chat triggering native submit event (#10224) 2024-10-04 20:31:25 +05:30
909 changed files with 6409 additions and 7362 deletions
+113 -133
View File
@@ -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
+11
View File
@@ -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
View File
@@ -27,7 +27,8 @@ checks:
threshold: 50
exclude_patterns:
- 'spec/'
- '**/specs/'
- '**/specs/**/**'
- '**/spec/**/**'
- 'db/*'
- 'bin/**/*'
- 'db/**/*'
+24 -1
View File
@@ -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,
},
};
+43
View File
@@ -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
+39 -36
View File
@@ -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
+14 -4
View File
@@ -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>
@@ -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');
});
@@ -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', () => {
@@ -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"
@@ -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