Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ed5a4b4f | ||
|
|
7ecb993d06 | ||
|
|
c00a157c03 | ||
|
|
3750689bee | ||
|
|
0511bf4ba6 | ||
|
|
9f50638a40 | ||
|
|
f5ac272e3b | ||
|
|
2ba7318817 | ||
|
|
412303d1cd | ||
|
|
47b9399208 | ||
|
|
0830ad78b6 | ||
|
|
0b8df762f9 | ||
|
|
f24c87f9b2 | ||
|
|
f62a4170b6 | ||
|
|
2f090dbc61 | ||
|
|
ec17682d7a | ||
|
|
7861ce8c47 | ||
|
|
e5de41fea4 | ||
|
|
adb0e94cbd | ||
|
|
42fb083491 | ||
|
|
9101e1e221 | ||
|
|
91bac16c29 | ||
|
|
97699f3087 | ||
|
|
1242cfd704 | ||
|
|
6963c668bf | ||
|
|
a8c12ffb25 | ||
|
|
97d4ce65c4 | ||
|
|
ea787fecd8 | ||
|
|
ee02923ace | ||
|
|
0677d8763d | ||
|
|
095aaf3de6 | ||
|
|
b7aea99f44 | ||
|
|
d0bf66fe7a | ||
|
|
490fc4eaba | ||
|
|
e6e0f7de5a | ||
|
|
d2eda8bd08 | ||
|
|
55bf05d2d4 | ||
|
|
5b22af6af8 | ||
|
|
9338bc1391 | ||
|
|
88a16b8e96 | ||
|
|
8c54d7f794 | ||
|
|
bd88bfb0fe | ||
|
|
83100b8f60 | ||
|
|
0f26351fd3 | ||
|
|
6d053e5816 | ||
|
|
bbb8e57fee | ||
|
|
5eac95732b | ||
|
|
b3262597c1 | ||
|
|
701135df92 | ||
|
|
fdc3e370ef | ||
|
|
f14edd5242 | ||
|
|
578dce81a1 | ||
|
|
c51a458c25 | ||
|
|
b8d0252511 | ||
|
|
edc1fe2363 | ||
|
|
0491ffc94e |
+113
-133
@@ -1,84 +1,87 @@
|
|||||||
# Ruby CircleCI 2.0 configuration file
|
version: 2.1
|
||||||
#
|
orbs:
|
||||||
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
node: circleci/node@6.1.0
|
||||||
#
|
|
||||||
version: 2
|
|
||||||
defaults: &defaults
|
defaults: &defaults
|
||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
machine:
|
||||||
# specify the version you desire here
|
image: ubuntu-2204:2024.05.1
|
||||||
- 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
|
|
||||||
resource_class: large
|
resource_class: large
|
||||||
|
environment:
|
||||||
|
RAILS_LOG_TO_STDOUT: false
|
||||||
|
COVERAGE: true
|
||||||
|
LOG_LEVEL: warn
|
||||||
|
parallelism: 4
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- 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:
|
- run:
|
||||||
name: Configure Bundler
|
name: Install System Dependencies
|
||||||
command: |
|
command: |
|
||||||
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
|
sudo apt-get update
|
||||||
source $BASH_ENV
|
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
|
gem install bundler
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Which bundler?
|
name: Install Application Dependencies
|
||||||
command: bundle -v
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Swap node versions
|
|
||||||
command: |
|
command: |
|
||||||
set +e
|
source ~/.rvm/scripts/rvm
|
||||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
|
bundle install
|
||||||
export NVM_DIR="$HOME/.nvm"
|
# pnpm install
|
||||||
[ -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
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Download cc-test-reporter
|
name: Download cc-test-reporter
|
||||||
@@ -86,12 +89,8 @@ jobs:
|
|||||||
mkdir -p ~/tmp
|
mkdir -p ~/tmp
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||||
chmod +x ~/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:
|
- run:
|
||||||
name: Verify swagger API specification
|
name: Verify swagger API specification
|
||||||
command: |
|
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
|
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
|
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||||
|
|
||||||
# Database setup
|
# we remove the FRONTED_URL from the .env before running the tests
|
||||||
- run: bundle exec rake db:create
|
- run:
|
||||||
- run: bundle exec rake db:schema:load
|
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:
|
- run:
|
||||||
name: Bundle audit
|
name: Bundle audit
|
||||||
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||||
|
|
||||||
|
# Rubocop linting
|
||||||
- run:
|
- run:
|
||||||
name: Rubocop
|
name: Rubocop
|
||||||
command: bundle exec rubocop
|
command: bundle exec rubocop
|
||||||
|
|
||||||
# - run:
|
# ESLint linting
|
||||||
# name: Brakeman
|
|
||||||
# command: bundle exec brakeman
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: eslint
|
name: eslint
|
||||||
command: yarn run eslint
|
command: pnpm run eslint
|
||||||
|
|
||||||
# Run frontend tests
|
|
||||||
- run:
|
- run:
|
||||||
name: Run frontend tests
|
name: Run frontend tests
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp/test-results/frontend_specs
|
mkdir -p ~/build/coverage/frontend
|
||||||
~/tmp/cc-test-reporter before-build
|
~/tmp/cc-test-reporter before-build
|
||||||
yarn test:coverage
|
pnpm run 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"
|
|
||||||
|
|
||||||
# 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:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp/test-results/rspec
|
mkdir -p ~/tmp/test-results/rspec
|
||||||
mkdir -p ~/tmp/test-artifacts
|
mkdir -p ~/tmp/test-artifacts
|
||||||
mkdir -p coverage
|
mkdir -p ~/build/coverage/backend
|
||||||
~/tmp/cc-test-reporter before-build
|
~/tmp/cc-test-reporter before-build
|
||||||
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||||
bundle exec rspec --format progress \
|
bundle exec rspec --format progress \
|
||||||
@@ -150,54 +166,18 @@ jobs:
|
|||||||
--out ~/tmp/test-results/rspec.xml \
|
--out ~/tmp/test-results/rspec.xml \
|
||||||
-- ${TESTFILES}
|
-- ${TESTFILES}
|
||||||
no_output_timeout: 30m
|
no_output_timeout: 30m
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Code Climate Test Coverage
|
name: Code Climate Test Coverage (Backend)
|
||||||
command: |
|
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:
|
- persist_to_workspace:
|
||||||
root: coverage
|
root: ~/build
|
||||||
paths:
|
paths:
|
||||||
- codeclimate.*.json
|
- coverage
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE USER chatwoot CREATEDB;
|
||||||
|
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
|
||||||
|
ALTER ROLE chatwoot SUPERUSER;
|
||||||
|
|
||||||
|
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
|
||||||
|
DROP DATABASE template1;
|
||||||
|
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
|
||||||
|
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
|
||||||
|
|
||||||
|
\c template1;
|
||||||
|
VACUUM FREEZE;
|
||||||
+2
-1
@@ -27,7 +27,8 @@ checks:
|
|||||||
threshold: 50
|
threshold: 50
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- 'spec/'
|
- 'spec/'
|
||||||
- '**/specs/'
|
- '**/specs/**/**'
|
||||||
|
- '**/spec/**/**'
|
||||||
- 'db/*'
|
- 'db/*'
|
||||||
- 'bin/**/*'
|
- 'bin/**/*'
|
||||||
- 'db/**/*'
|
- 'db/**/*'
|
||||||
|
|||||||
+24
-1
@@ -1,6 +1,23 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/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'],
|
plugins: ['html', 'prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'prettier/prettier': ['error'],
|
'prettier/prettier': ['error'],
|
||||||
camelcase: 'off',
|
camelcase: 'off',
|
||||||
@@ -206,5 +223,11 @@ module.exports = {
|
|||||||
globals: {
|
globals: {
|
||||||
bus: true,
|
bus: true,
|
||||||
vi: true,
|
vi: true,
|
||||||
|
// beforeEach: true,
|
||||||
|
// afterEach: true,
|
||||||
|
// test: true,
|
||||||
|
// describe: true,
|
||||||
|
// it: true,
|
||||||
|
// expect: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Frontend Lint & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9.3.0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run eslint
|
||||||
|
run: pnpm run eslint
|
||||||
|
|
||||||
|
- name: Run frontend tests with coverage
|
||||||
|
run: |
|
||||||
|
mkdir -p coverage
|
||||||
|
pnpm run test:coverage
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
image: postgres:15.3
|
image: postgres:15.3
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: ""
|
POSTGRES_PASSWORD: ''
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
ports:
|
ports:
|
||||||
@@ -41,46 +41,49 @@ jobs:
|
|||||||
options: --entrypoint redis-server
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install pnpm dependencies
|
- name: Install pnpm dependencies
|
||||||
run: pnpm i
|
run: pnpm i
|
||||||
|
|
||||||
- name: Strip enterprise code
|
- name: Strip enterprise code
|
||||||
run: |
|
run: |
|
||||||
rm -rf enterprise
|
rm -rf enterprise
|
||||||
rm -rf spec/enterprise
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
- name: Create database
|
- name: Create database
|
||||||
run: bundle exec rake db:create
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
- name: Seed database
|
- name: Seed database
|
||||||
run: bundle exec rake db:schema:load
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
# Run rails tests
|
- name: Run frontend tests
|
||||||
- name: Run backend tests
|
run: pnpm run test:coverage
|
||||||
run: |
|
|
||||||
bundle exec rspec --profile=10 --format documentation
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --openssl-legacy-provider
|
|
||||||
|
|
||||||
- name: Upload rails log folder
|
# Run rails tests
|
||||||
uses: actions/upload-artifact@v4
|
- name: Run backend tests
|
||||||
if: always()
|
run: |
|
||||||
with:
|
bundle exec rspec --profile=10 --format documentation
|
||||||
name: rails-log-folder
|
env:
|
||||||
path: log
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
|
- name: Upload rails log folder
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: rails-log-folder
|
||||||
|
path: log
|
||||||
|
|||||||
@@ -49,13 +49,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 9.3.0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: yarn
|
cache: pnpm
|
||||||
|
|
||||||
- name: yarn
|
- name: pnpm
|
||||||
run: yarn install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Create database
|
- name: Create database
|
||||||
run: bundle exec rake db:create
|
run: bundle exec rake db:create
|
||||||
|
|||||||
@@ -19,23 +19,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 9.3.0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'yarn'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: yarn
|
- name: pnpm
|
||||||
run: yarn install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Strip enterprise code
|
- name: Strip enterprise code
|
||||||
run: |
|
run: |
|
||||||
rm -rf enterprise
|
rm -rf enterprise
|
||||||
rm -rf spec/enterprise
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: setup env
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
- name: Run asset compile
|
- name: Run asset compile
|
||||||
run: bundle exec rake assets:precompile
|
run: bundle exec rake assets:precompile
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --openssl-legacy-provider
|
RAILS_ENV: production
|
||||||
|
|
||||||
- name: Size Check
|
- name: Size Check
|
||||||
run: yarn run size
|
run: pnpm run size
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.9.0
|
3.13.0
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.8.0
|
3.0.0
|
||||||
|
|||||||
@@ -52,8 +52,11 @@ export default {
|
|||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
authUIFlags: 'getAuthUIFlags',
|
authUIFlags: 'getAuthUIFlags',
|
||||||
accountUIFlags: 'accounts/getUIFlags',
|
accountUIFlags: 'accounts/getUIFlags',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
// currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
|
currentAccountId() {
|
||||||
|
return this.$route.params.accountId;
|
||||||
|
},
|
||||||
hasAccounts() {
|
hasAccounts() {
|
||||||
const { accounts = [] } = this.currentUser || {};
|
const { accounts = [] } = this.currentUser || {};
|
||||||
return accounts.length > 0;
|
return accounts.length > 0;
|
||||||
@@ -70,15 +73,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentAccountId() {
|
currentAccountId() {
|
||||||
|
console.log('THIS CALL', this.currentAccountId);
|
||||||
if (this.currentAccountId) {
|
if (this.currentAccountId) {
|
||||||
this.initializeAccount();
|
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.initializeColorTheme();
|
||||||
this.listenToThemeChanges();
|
this.listenToThemeChanges();
|
||||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
await this.$store.dispatch('accounts/get');
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.reconnectService) {
|
if (this.reconnectService) {
|
||||||
@@ -94,7 +104,7 @@ export default {
|
|||||||
mql.onchange = e => setColorTheme(e.matches);
|
mql.onchange = e => setColorTheme(e.matches);
|
||||||
},
|
},
|
||||||
setLocale(locale) {
|
setLocale(locale) {
|
||||||
this.$root.$i18n.locale = locale;
|
this.$i18n.locale = locale;
|
||||||
},
|
},
|
||||||
async initializeAccount() {
|
async initializeAccount() {
|
||||||
await this.$store.dispatch('accounts/get');
|
await this.$store.dispatch('accounts/get');
|
||||||
|
|||||||
@@ -537,12 +537,6 @@ function loadMoreConversations() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the current page
|
|
||||||
store.dispatch('conversationPage/setCurrentPage', {
|
|
||||||
filter: currentPageFilterKey.value,
|
|
||||||
page: currentFiltersPage.value + 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasAppliedFiltersOrActiveFolders.value) {
|
if (!hasAppliedFiltersOrActiveFolders.value) {
|
||||||
fetchConversations();
|
fetchConversations();
|
||||||
} else if (hasActiveFolders.value) {
|
} else if (hasActiveFolders.value) {
|
||||||
@@ -769,11 +763,11 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
:has-applied-filters="hasAppliedFilters"
|
:has-applied-filters="hasAppliedFilters"
|
||||||
:has-active-folders="hasActiveFolders"
|
:has-active-folders="hasActiveFolders"
|
||||||
:active-status="activeStatus"
|
:active-status="activeStatus"
|
||||||
@addFolders="onClickOpenAddFoldersModal"
|
@add-folders="onClickOpenAddFoldersModal"
|
||||||
@deleteFolders="onClickOpenDeleteFoldersModal"
|
@delete-folders="onClickOpenDeleteFoldersModal"
|
||||||
@filtersModal="onToggleAdvanceFiltersModal"
|
@filters-modal="onToggleAdvanceFiltersModal"
|
||||||
@resetFilters="resetAndFetchData"
|
@reset-filters="resetAndFetchData"
|
||||||
@basicFilterChange="onBasicFilterChange"
|
@basic-filter-change="onBasicFilterChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddCustomViews
|
<AddCustomViews
|
||||||
@@ -785,7 +779,7 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
|
|
||||||
<DeleteCustomViews
|
<DeleteCustomViews
|
||||||
v-if="showDeleteFoldersModal"
|
v-if="showDeleteFoldersModal"
|
||||||
:show-delete-popup.sync="showDeleteFoldersModal"
|
v-model:show="showDeleteFoldersModal"
|
||||||
:active-custom-view="activeFolder"
|
:active-custom-view="activeFolder"
|
||||||
:custom-views-id="foldersId"
|
:custom-views-id="foldersId"
|
||||||
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
|
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
|
||||||
@@ -796,7 +790,7 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||||
:items="assigneeTabItems"
|
:items="assigneeTabItems"
|
||||||
:active-tab="activeAssigneeTab"
|
:active-tab="activeAssigneeTab"
|
||||||
@chatTabChange="updateAssigneeTab"
|
@chat-tab-change="updateAssigneeTab"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@@ -813,11 +807,11 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
:show-open-action="allSelectedConversationsStatus('open')"
|
:show-open-action="allSelectedConversationsStatus('open')"
|
||||||
:show-resolved-action="allSelectedConversationsStatus('resolved')"
|
:show-resolved-action="allSelectedConversationsStatus('resolved')"
|
||||||
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
|
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
|
||||||
@selectAllConversations="toggleSelectAll"
|
@select-all-conversations="toggleSelectAll"
|
||||||
@assignAgent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@updateConversations="onUpdateConversations"
|
@update-conversations="onUpdateConversations"
|
||||||
@assignLabels="onAssignLabels"
|
@assign-labels="onAssignLabels"
|
||||||
@assignTeam="onAssignTeamsForBulk"
|
@assign-team="onAssignTeamsForBulk"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="conversationListRef"
|
ref="conversationListRef"
|
||||||
@@ -854,8 +848,8 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
:folders-id="foldersId"
|
:folders-id="foldersId"
|
||||||
:conversation-type="conversationType"
|
:conversation-type="conversationType"
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
:show-assignee="showAssigneeInConversationCard"
|
||||||
@selectConversation="selectConversation"
|
@select-conversation="selectConversation"
|
||||||
@deSelectConversation="deSelectConversation"
|
@de-select-conversation="deSelectConversation"
|
||||||
/>
|
/>
|
||||||
</DynamicScrollerItem>
|
</DynamicScrollerItem>
|
||||||
</template>
|
</template>
|
||||||
@@ -878,7 +872,7 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
</DynamicScroller>
|
</DynamicScroller>
|
||||||
</div>
|
</div>
|
||||||
<woot-modal
|
<woot-modal
|
||||||
:show.sync="showAdvancedFilters"
|
v-model:show="showAdvancedFilters"
|
||||||
:on-close="closeAdvanceFiltersModal"
|
:on-close="closeAdvanceFiltersModal"
|
||||||
size="medium"
|
size="medium"
|
||||||
>
|
>
|
||||||
@@ -889,8 +883,8 @@ watch(conversationFilters, (newVal, oldVal) => {
|
|||||||
:active-folder-name="activeFolderName"
|
:active-folder-name="activeFolderName"
|
||||||
:on-close="closeAdvanceFiltersModal"
|
:on-close="closeAdvanceFiltersModal"
|
||||||
:is-folder-view="hasActiveFolders"
|
:is-folder-view="hasActiveFolders"
|
||||||
@applyFilter="onApplyFilter"
|
@apply-filter="onApplyFilter"
|
||||||
@updateFolder="onUpdateSavedFilter"
|
@update-folder="onUpdateSavedFilter"
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
|
|||||||
/>
|
/>
|
||||||
<ConversationBasicFilter
|
<ConversationBasicFilter
|
||||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||||
@changeFilter="onBasicFilterChange"
|
@change-filter="onBasicFilterChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ export default {
|
|||||||
:selected="isConversationSelected(source.id)"
|
:selected="isConversationSelected(source.id)"
|
||||||
:show-assignee="showAssignee"
|
:show-assignee="showAssignee"
|
||||||
enable-context-menu
|
enable-context-menu
|
||||||
@selectConversation="selectConversation"
|
@select-conversation="selectConversation"
|
||||||
@deSelectConversation="deSelectConversation"
|
@de-select-conversation="deSelectConversation"
|
||||||
@assignAgent="assignAgent"
|
@assign-agent="assignAgent"
|
||||||
@assignTeam="assignTeam"
|
@assign-team="assignTeam"
|
||||||
@assignLabel="assignLabels"
|
@assign-label="assignLabels"
|
||||||
@updateConversationStatus="updateConversationStatus"
|
@update-conversation-status="updateConversationStatus"
|
||||||
@contextMenuToggle="toggleContextMenu"
|
@context-menu-toggle="toggleContextMenu"
|
||||||
@markAsUnread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@assignPriority="assignPriority"
|
@assign-priority="assignPriority"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
attributeKey: { type: String, required: true },
|
attributeKey: { type: String, required: true },
|
||||||
contactId: { type: Number, default: null },
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
|
emits: ['update', 'delete', 'copy'],
|
||||||
setup() {
|
setup() {
|
||||||
return { v$: useVuelidate() };
|
return { v$: useVuelidate() };
|
||||||
},
|
},
|
||||||
@@ -138,7 +139,7 @@ export default {
|
|||||||
this.editedValue = this.formattedValue;
|
this.editedValue = this.formattedValue;
|
||||||
emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||||
},
|
},
|
||||||
destroyed() {
|
unmounted() {
|
||||||
emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const shouldShowContent = computed(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="shouldShowContent">
|
<div v-if="shouldShowContent">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
DatePicker,
|
DatePicker,
|
||||||
},
|
},
|
||||||
|
emits: ['close', 'chooseTime'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -43,15 +44,18 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
|
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
|
||||||
<form class="modal-content" @submit.prevent="chooseTime">
|
<form
|
||||||
|
class="modal-content w-full pt-2 px-5 pb-6"
|
||||||
|
@submit.prevent="chooseTime"
|
||||||
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-model:value="snoozeTime"
|
v-model:value="snoozeTime"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
inline
|
inline
|
||||||
|
input-class="mx-input reset-base"
|
||||||
:lang="lang"
|
:lang="lang"
|
||||||
:disabled-date="disabledDate"
|
:disabled-date="disabledDate"
|
||||||
:disabled-time="disabledTime"
|
:disabled-time="disabledTime"
|
||||||
:popup-style="{ width: '100%' }"
|
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||||
<woot-button variant="clear" @click.prevent="onClose">
|
<woot-button variant="clear" @click.prevent="onClose">
|
||||||
@@ -64,9 +68,3 @@ export default {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.modal-content {
|
|
||||||
@apply pt-2 px-5 pb-6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
import { ref, computed, defineEmits, onMounted } from 'vue';
|
import { ref, computed, defineEmits, onMounted } from 'vue';
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
|
||||||
const { show, modalType, closeOnBackdropClick, onClose } = defineProps({
|
const { modalType, closeOnBackdropClick, onClose } = defineProps({
|
||||||
closeOnBackdropClick: { type: Boolean, default: true },
|
closeOnBackdropClick: { type: Boolean, default: true },
|
||||||
show: Boolean,
|
|
||||||
showCloseButton: { type: Boolean, default: true },
|
showCloseButton: { type: Boolean, default: true },
|
||||||
onClose: { type: Function, required: true },
|
onClose: { type: Function, required: true },
|
||||||
fullWidth: { type: Boolean, default: false },
|
fullWidth: { type: Boolean, default: false },
|
||||||
@@ -14,6 +13,7 @@ const { show, modalType, closeOnBackdropClick, onClose } = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
const show = defineModel('show', { type: Boolean, default: false });
|
||||||
|
|
||||||
const modalClassName = computed(() => {
|
const modalClassName = computed(() => {
|
||||||
const modalClassNameMap = {
|
const modalClassNameMap = {
|
||||||
@@ -32,6 +32,7 @@ const handleMouseDown = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
show.value = false;
|
||||||
emit('close');
|
emit('close');
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -40,14 +41,14 @@ const onMouseUp = () => {
|
|||||||
if (mousedDownOnBackdrop.value) {
|
if (mousedDownOnBackdrop.value) {
|
||||||
mousedDownOnBackdrop.value = false;
|
mousedDownOnBackdrop.value = false;
|
||||||
if (closeOnBackdropClick) {
|
if (closeOnBackdropClick) {
|
||||||
onClose();
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeydown = e => {
|
const onKeydown = e => {
|
||||||
if (show && e.code === 'Escape') {
|
if (show.value && e.code === 'Escape') {
|
||||||
onClose();
|
close();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export default {
|
|||||||
<div class="flex flex-col items-start px-8 pt-8 pb-0">
|
<div class="flex flex-col items-start px-8 pt-8 pb-0">
|
||||||
<img v-if="headerImage" :src="headerImage" alt="No image" />
|
<img v-if="headerImage" :src="headerImage" alt="No image" />
|
||||||
<h2
|
<h2
|
||||||
ref="modalHeaderTitle"
|
data-test-id="modal-header-title"
|
||||||
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
|
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
|
||||||
>
|
>
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
v-if="headerContent"
|
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"
|
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
{{ headerContent }}
|
{{ headerContent }}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="shouldShowBanner"
|
v-if="shouldShowBanner"
|
||||||
@@ -86,6 +87,6 @@ export default {
|
|||||||
:banner-message="bannerMessage"
|
:banner-message="bannerMessage"
|
||||||
:action-button-label="actionButtonMessage"
|
:action-button-label="actionButtonMessage"
|
||||||
has-action-button
|
has-action-button
|
||||||
@primaryAction="routeToBilling"
|
@primary-action="routeToBilling"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="shouldShowBanner"
|
v-if="shouldShowBanner"
|
||||||
@@ -36,6 +37,6 @@ export default {
|
|||||||
:action-button-label="actionButtonMessage"
|
:action-button-label="actionButtonMessage"
|
||||||
action-button-icon="mail"
|
action-button-icon="mail"
|
||||||
has-action-button
|
has-action-button
|
||||||
@primaryAction="resendVerificationEmail"
|
@primary-action="resendVerificationEmail"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="shouldShowBanner"
|
v-if="shouldShowBanner"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="shouldShowBanner"
|
v-if="shouldShowBanner"
|
||||||
@@ -88,6 +89,6 @@ export default {
|
|||||||
:banner-message="bannerMessage"
|
:banner-message="bannerMessage"
|
||||||
:action-button-label="actionButtonMessage"
|
:action-button-label="actionButtonMessage"
|
||||||
has-action-button
|
has-action-button
|
||||||
@primaryAction="routeToBilling"
|
@primary-action="routeToBilling"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default {
|
|||||||
size="small"
|
size="small"
|
||||||
:color-scheme="status.disabled ? '' : 'secondary'"
|
:color-scheme="status.disabled ? '' : 'secondary'"
|
||||||
:variant="status.disabled ? 'smooth' : 'clear'"
|
:variant="status.disabled ? 'smooth' : 'clear'"
|
||||||
class-names="status-change--dropdown-button"
|
class="status-change--dropdown-button"
|
||||||
@click="changeAvailabilityStatus(status.value)"
|
@click="changeAvailabilityStatus(status.value)"
|
||||||
>
|
>
|
||||||
<AvailabilityStatusBadge :status="status.value" />
|
<AvailabilityStatusBadge :status="status.value" />
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
hasBanner: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
emits: [
|
||||||
|
'toggleAccountModal',
|
||||||
|
'showAddLabelPopup',
|
||||||
|
'openNotificationPanel',
|
||||||
|
'closeKeyShortcutModal',
|
||||||
|
'openKeyShortcutModal',
|
||||||
|
],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -117,6 +120,7 @@ export default {
|
|||||||
this.currentUser,
|
this.currentUser,
|
||||||
this.accountId
|
this.accountId
|
||||||
);
|
);
|
||||||
|
|
||||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||||
return menuItems.filter(menuItem => {
|
return menuItems.filter(menuItem => {
|
||||||
const isAvailableForTheUser = hasPermissions(
|
const isAvailableForTheUser = hasPermissions(
|
||||||
@@ -127,6 +131,7 @@ export default {
|
|||||||
if (!isAvailableForTheUser) {
|
if (!isAvailableForTheUser) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
menuItem.alwaysVisibleOnChatwootInstances &&
|
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||||
!this.isACustomBrandedInstance
|
!this.isACustomBrandedInstance
|
||||||
@@ -134,10 +139,12 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (menuItem.featureFlag) {
|
if (menuItem.featureFlag) {
|
||||||
return this.isFeatureEnabledonAccount(
|
const isEnabled = this.isFeatureEnabledonAccount(
|
||||||
this.accountId,
|
this.accountId,
|
||||||
menuItem.featureFlag
|
menuItem.featureFlag
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return isEnabled;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -217,9 +224,9 @@ export default {
|
|||||||
:account-id="accountId"
|
:account-id="accountId"
|
||||||
:menu-items="primaryMenuItems"
|
:menu-items="primaryMenuItems"
|
||||||
:active-menu-item="activePrimaryMenu.key"
|
:active-menu-item="activePrimaryMenu.key"
|
||||||
@toggleAccounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
@openKeyShortcutModal="toggleKeyShortcutModal"
|
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@openNotificationPanel="openNotificationPanel"
|
@open-notification-panel="openNotificationPanel"
|
||||||
/>
|
/>
|
||||||
<SecondarySidebar
|
<SecondarySidebar
|
||||||
v-if="hasSecondarySidebar"
|
v-if="hasSecondarySidebar"
|
||||||
@@ -231,8 +238,8 @@ export default {
|
|||||||
:menu-config="activeSecondaryMenu"
|
:menu-config="activeSecondaryMenu"
|
||||||
:current-user="currentUser"
|
:current-user="currentUser"
|
||||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||||
@addLabel="showAddLabelPopup"
|
@add-label="showAddLabelPopup"
|
||||||
@toggleAccounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['toggleAccounts'],
|
||||||
data() {
|
data() {
|
||||||
return { showSwitchButton: false };
|
return { showSwitchButton: false };
|
||||||
},
|
},
|
||||||
@@ -25,6 +26,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="showShowCurrentAccountContext"
|
v-if="showShowCurrentAccountContext"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['closeAccountModal', 'showCreateAccountModal'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['closeAccountCreateModal'],
|
||||||
setup() {
|
setup() {
|
||||||
return { v$: useVuelidate() };
|
return { v$: useVuelidate() };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
|
emits: ['toggleMenu'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['openNotificationPanel'],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
notificationMetadata: 'notifications/getMeta',
|
notificationMetadata: 'notifications/getMeta',
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: [
|
||||||
|
'close',
|
||||||
|
'openKeyShortcutModal',
|
||||||
|
'toggleAccounts',
|
||||||
|
'showSupportChatWindow',
|
||||||
|
],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
|
|||||||
@@ -43,12 +43,16 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['toggleAccounts', 'openNotificationPanel', 'openKeyShortcutModal'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
helpDocsURL: wootConstants.DOCS_URL,
|
helpDocsURL: wootConstants.DOCS_URL,
|
||||||
showOptionsMenu: false,
|
showOptionsMenu: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log('MENU ITEMS', this.menuItems);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
frontendURL,
|
frontendURL,
|
||||||
toggleOptions() {
|
toggleOptions() {
|
||||||
@@ -96,13 +100,13 @@ export default {
|
|||||||
open-in-new-page
|
open-in-new-page
|
||||||
:to="helpDocsURL"
|
:to="helpDocsURL"
|
||||||
/>
|
/>
|
||||||
<NotificationBell @openNotificationPanel="openNotificationPanel" />
|
<NotificationBell @open-notification-panel="openNotificationPanel" />
|
||||||
<AgentDetails @toggleMenu="toggleOptions" />
|
<AgentDetails @toggle-menu="toggleOptions" />
|
||||||
<OptionsMenu
|
<OptionsMenu
|
||||||
:show="showOptionsMenu"
|
:show="showOptionsMenu"
|
||||||
@toggleAccounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
@showSupportChatWindow="toggleSupportChatWindow"
|
@show-support-chat-window="toggleSupportChatWindow"
|
||||||
@openKeyShortcutModal="$emit('openKeyShortcutModal')"
|
@open-key-shortcut-modal="$emit('openKeyShortcutModal')"
|
||||||
@close="toggleOptions"
|
@close="toggleOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log(
|
||||||
|
'MOUNTED: PRIMARY NAV ITEM',
|
||||||
|
this.name,
|
||||||
|
this.$t(`SIDEBAR.${this.name}`)
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['addLabel', 'toggleAccounts'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
@@ -242,7 +243,7 @@ export default {
|
|||||||
<div
|
<div
|
||||||
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
|
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
|
||||||
>
|
>
|
||||||
<AccountContext @toggleAccounts="toggleAccountModal" />
|
<AccountContext @toggle-accounts="toggleAccountModal" />
|
||||||
<transition-group
|
<transition-group
|
||||||
name="menu-list"
|
name="menu-list"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
@@ -257,7 +258,7 @@ export default {
|
|||||||
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
|
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
|
||||||
:key="menuItem.key"
|
:key="menuItem.key"
|
||||||
:menu-item="menuItem"
|
:menu-item="menuItem"
|
||||||
@addLabel="showAddLabelPopup"
|
@add-label="showAddLabelPopup"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-2
@@ -106,10 +106,11 @@ export default {
|
|||||||
<span
|
<span
|
||||||
v-if="showChildCount"
|
v-if="showChildCount"
|
||||||
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1"
|
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1"
|
||||||
:class="isCountZero
|
:class="
|
||||||
|
isCountZero
|
||||||
? `text-slate-300 dark:text-slate-500`
|
? `text-slate-300 dark:text-slate-500`
|
||||||
: `text-slate-700 dark:text-slate-50`
|
: `text-slate-700 dark:text-slate-50`
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ childItemCount }}
|
{{ childItemCount }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['addLabel', 'open'],
|
||||||
setup() {
|
setup() {
|
||||||
const { isAdmin } = useAdmin();
|
const { isAdmin } = useAdmin();
|
||||||
const { isEnterprise } = useConfig();
|
const { isEnterprise } = useConfig();
|
||||||
|
|||||||
+42
-59
@@ -1,79 +1,62 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { createStore } from 'vuex';
|
||||||
import AccountSelector from '../AccountSelector.vue';
|
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 WootModal from 'dashboard/components/Modal.vue';
|
||||||
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
|
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const store = createStore({
|
||||||
localVue.component('woot-modal', WootModal);
|
modules: {
|
||||||
localVue.component('woot-modal-header', WootModalHeader);
|
auth: {
|
||||||
localVue.component('fluent-icon', FluentIcon);
|
namespaced: false,
|
||||||
|
getters: {
|
||||||
localVue.use(Vuex);
|
getCurrentAccountId: () => 1,
|
||||||
localVue.use(VueI18n);
|
getCurrentUser: () => ({
|
||||||
|
accounts: [
|
||||||
const i18nConfig = new VueI18n({
|
{ id: 1, name: 'Chatwoot', role: 'administrator' },
|
||||||
locale: 'en',
|
{ id: 2, name: 'GitX', role: 'agent' },
|
||||||
messages: i18n,
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalConfig: {
|
||||||
|
namespaced: true,
|
||||||
|
getters: {
|
||||||
|
get: () => ({ createNewAccountFromDashboard: false }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('accountSelctor', () => {
|
describe('AccountSelector', () => {
|
||||||
let accountSelector = null;
|
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(() => {
|
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, {
|
accountSelector = mount(AccountSelector, {
|
||||||
store,
|
global: {
|
||||||
localVue,
|
plugins: [store],
|
||||||
i18n: i18nConfig,
|
components: {
|
||||||
propsData: { showAccountModal: true },
|
'woot-modal': WootModal,
|
||||||
stubs: { WootButton: { template: '<button />' } },
|
'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', () => {
|
it('title and sub title exist', () => {
|
||||||
const headerComponent = accountSelector.findComponent(WootModalHeader);
|
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');
|
expect(title.text()).toBe('Switch Account');
|
||||||
const content = headerComponent.findComponent({
|
const content = headerComponent.find(
|
||||||
ref: 'modalHeaderContent',
|
'[data-test-id="modal-header-content"]'
|
||||||
});
|
);
|
||||||
expect(content.text()).toBe('Select an account from the following list');
|
expect(content.text()).toBe('Select an account from the following list');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+32
-40
@@ -1,27 +1,10 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { createStore } from 'vuex';
|
||||||
import AgentDetails from '../AgentDetails.vue';
|
import 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 Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import WootButton from 'dashboard/components/ui/WootButton.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({
|
describe('AgentDetails', () => {
|
||||||
locale: 'en',
|
|
||||||
messages: i18n,
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('agentDetails', () => {
|
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
name: 'Neymar Junior',
|
name: 'Neymar Junior',
|
||||||
avatar_url: '',
|
avatar_url: '',
|
||||||
@@ -29,37 +12,46 @@ describe('agentDetails', () => {
|
|||||||
};
|
};
|
||||||
const currentRole = 'agent';
|
const currentRole = 'agent';
|
||||||
let store = null;
|
let store = null;
|
||||||
let actions = null;
|
|
||||||
let modules = null;
|
|
||||||
let agentDetails = null;
|
let agentDetails = null;
|
||||||
|
|
||||||
beforeEach(() => {
|
const mockTooltipDirective = {
|
||||||
actions = {};
|
mounted: (el, binding) => {
|
||||||
|
// You can mock the behavior here if necessary
|
||||||
|
el.setAttribute('data-tooltip', binding.value || '');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
modules = {
|
beforeEach(() => {
|
||||||
auth: {
|
store = createStore({
|
||||||
getters: {
|
modules: {
|
||||||
getCurrentUser: () => currentUser,
|
auth: {
|
||||||
getCurrentRole: () => currentRole,
|
namespaced: false,
|
||||||
getCurrentUserAvailability: () => currentUser.availability_status,
|
getters: {
|
||||||
|
getCurrentUser: () => currentUser,
|
||||||
|
getCurrentRole: () => currentRole,
|
||||||
|
getCurrentUserAvailability: () => currentUser.availability_status,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
store = new Vuex.Store({
|
|
||||||
actions,
|
|
||||||
modules,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
agentDetails = shallowMount(AgentDetails, {
|
agentDetails = shallowMount(AgentDetails, {
|
||||||
store,
|
global: {
|
||||||
localVue,
|
plugins: [store],
|
||||||
i18n: i18nConfig,
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
WootButton,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip: mockTooltipDirective, // Mocking the tooltip directive
|
||||||
|
},
|
||||||
|
stubs: { WootButton: { template: '<button><slot /></button>' } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(' the agent status', () => {
|
it('shows the correct agent status', () => {
|
||||||
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
|
expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('agent thumbnail exists', () => {
|
it('agent thumbnail exists', () => {
|
||||||
|
|||||||
+32
-34
@@ -1,20 +1,7 @@
|
|||||||
import NotificationBell from '../NotificationBell.vue';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
import { createStore } from 'vuex';
|
||||||
import Vuex from 'vuex';
|
|
||||||
import VueI18n from 'vue-i18n';
|
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||||
|
import NotificationBell from '../NotificationBell.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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const $route = {
|
const $route = {
|
||||||
name: 'notifications_index',
|
name: 'notifications_index',
|
||||||
@@ -33,43 +20,51 @@ describe('notificationBell', () => {
|
|||||||
};
|
};
|
||||||
modules = {
|
modules = {
|
||||||
auth: {
|
auth: {
|
||||||
|
namespaced: false,
|
||||||
getters: {
|
getters: {
|
||||||
getCurrentAccountId: () => accountId,
|
getCurrentAccountId: () => accountId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
|
namespaced: false,
|
||||||
getters: {
|
getters: {
|
||||||
'notifications/getMeta': () => notificationMetadata,
|
'notifications/getMeta': () => notificationMetadata,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
store = new Vuex.Store({
|
store = createStore({
|
||||||
actions,
|
actions,
|
||||||
modules,
|
modules,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 19 ', () => {
|
it('it should return unread count 19', () => {
|
||||||
const wrapper = shallowMount(NotificationBell, {
|
const wrapper = shallowMount(NotificationBell, {
|
||||||
localVue,
|
global: {
|
||||||
i18n: i18nConfig,
|
plugins: [store],
|
||||||
store,
|
mocks: {
|
||||||
mocks: {
|
$route,
|
||||||
$route,
|
},
|
||||||
|
components: {
|
||||||
|
'fluent-icon': FluentIcon,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.vm.unreadCount).toBe('19');
|
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;
|
notificationMetadata.unreadCount = 100;
|
||||||
const wrapper = shallowMount(NotificationBell, {
|
const wrapper = shallowMount(NotificationBell, {
|
||||||
localVue,
|
global: {
|
||||||
i18n: i18nConfig,
|
plugins: [store],
|
||||||
store,
|
mocks: {
|
||||||
mocks: {
|
$route,
|
||||||
$route,
|
},
|
||||||
|
components: {
|
||||||
|
'fluent-icon': FluentIcon,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.vm.unreadCount).toBe('99+');
|
expect(wrapper.vm.unreadCount).toBe('99+');
|
||||||
@@ -77,11 +72,14 @@ describe('notificationBell', () => {
|
|||||||
|
|
||||||
it('isNotificationPanelActive', async () => {
|
it('isNotificationPanelActive', async () => {
|
||||||
const notificationBell = shallowMount(NotificationBell, {
|
const notificationBell = shallowMount(NotificationBell, {
|
||||||
store,
|
global: {
|
||||||
localVue,
|
plugins: [store],
|
||||||
i18n: i18nConfig,
|
mocks: {
|
||||||
mocks: {
|
$route,
|
||||||
$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 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 WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.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 WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.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', () => {
|
describe('AvailabilityStatus', () => {
|
||||||
const currentAvailability = 'online';
|
const currentAvailability = 'online';
|
||||||
const currentAccountId = '1';
|
const currentAccountId = '1';
|
||||||
const currentUserAutoOffline = false;
|
const currentUserAutoOffline = false;
|
||||||
let store = null;
|
let store = null;
|
||||||
let actions = null;
|
let actions = null;
|
||||||
let modules = null;
|
|
||||||
let availabilityStatus = null;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
actions = {
|
actions = {
|
||||||
updateAvailability: vi.fn(() => {
|
updateAvailability: vi.fn(() => Promise.resolve()),
|
||||||
return Promise.resolve();
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
modules = {
|
store = createStore({
|
||||||
auth: {
|
modules: {
|
||||||
getters: {
|
auth: {
|
||||||
getCurrentUserAvailability: () => currentAvailability,
|
namespaced: false,
|
||||||
getCurrentAccountId: () => currentAccountId,
|
getters: {
|
||||||
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
getCurrentUserAvailability: () => currentAvailability,
|
||||||
|
getCurrentAccountId: () => currentAccountId,
|
||||||
|
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
actions,
|
||||||
|
|
||||||
store = new Vuex.Store({ actions, modules });
|
|
||||||
|
|
||||||
availabilityStatus = mount(AvailabilityStatus, {
|
|
||||||
store,
|
|
||||||
localVue,
|
|
||||||
i18n: i18nConfig,
|
|
||||||
stubs: { WootSwitch: { template: '<button />' } },
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches an action when user changes status', async () => {
|
it('dispatches an action when user changes status', async () => {
|
||||||
await availabilityStatus;
|
const wrapper = mount(AvailabilityStatus, {
|
||||||
availabilityStatus
|
global: {
|
||||||
.findAll('.status-change--dropdown-button')
|
plugins: [store],
|
||||||
.at(2)
|
components: {
|
||||||
.trigger('click');
|
WootButton,
|
||||||
|
WootDropdownItem,
|
||||||
|
WootDropdownMenu,
|
||||||
|
WootDropdownHeader,
|
||||||
|
WootDropdownDivider,
|
||||||
|
FluentIcon,
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
WootSwitch: { template: '<button />' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(actions.updateAvailability).toBeCalledWith(
|
// Ensure that the dropdown menu is opened
|
||||||
expect.any(Object),
|
await wrapper.vm.openStatusMenu();
|
||||||
{ availability: 'offline', account_id: currentAccountId },
|
|
||||||
undefined
|
// 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"
|
icon="list"
|
||||||
size="small"
|
size="small"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
/>
|
>
|
||||||
|
|
||||||
|
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['open', 'close'],
|
||||||
methods: {
|
methods: {
|
||||||
onClickOpenPath() {
|
onClickOpenPath() {
|
||||||
this.$emit('open');
|
this.$emit('open');
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['primaryAction', 'close'],
|
||||||
computed: {
|
computed: {
|
||||||
bannerClasses() {
|
bannerClasses() {
|
||||||
const classList = [this.colorScheme];
|
const classList = [this.colorScheme];
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ const emitDateRange = () => {
|
|||||||
>
|
>
|
||||||
<CalendarDateRange
|
<CalendarDateRange
|
||||||
:selected-range="selectedRange"
|
:selected-range="selectedRange"
|
||||||
@setRange="setDateRange"
|
@set-range="setDateRange"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
|
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
|
||||||
@@ -264,15 +264,15 @@ const emitDateRange = () => {
|
|||||||
:calendar-type="calendar"
|
:calendar-type="calendar"
|
||||||
:start-current-date="startCurrentDate"
|
:start-current-date="startCurrentDate"
|
||||||
:end-current-date="endCurrentDate"
|
:end-current-date="endCurrentDate"
|
||||||
@selectYear="openCalendar($event, calendar, YEAR)"
|
@select-year="openCalendar($event, calendar, YEAR)"
|
||||||
/>
|
/>
|
||||||
<CalendarMonth
|
<CalendarMonth
|
||||||
v-else-if="calendarViews[calendar] === MONTH"
|
v-else-if="calendarViews[calendar] === MONTH"
|
||||||
:calendar-type="calendar"
|
:calendar-type="calendar"
|
||||||
:start-current-date="startCurrentDate"
|
:start-current-date="startCurrentDate"
|
||||||
:end-current-date="endCurrentDate"
|
:end-current-date="endCurrentDate"
|
||||||
@selectMonth="openCalendar($event, calendar)"
|
@select-month="openCalendar($event, calendar)"
|
||||||
@setView="setViewMode"
|
@set-view="setViewMode"
|
||||||
@prev="moveCalendar(calendar, 'prev', YEAR)"
|
@prev="moveCalendar(calendar, 'prev', YEAR)"
|
||||||
@next="moveCalendar(calendar, 'next', YEAR)"
|
@next="moveCalendar(calendar, 'next', YEAR)"
|
||||||
/>
|
/>
|
||||||
@@ -286,9 +286,9 @@ const emitDateRange = () => {
|
|||||||
:selected-end-date="selectedEndDate"
|
:selected-end-date="selectedEndDate"
|
||||||
:selecting-end-date="selectingEndDate"
|
:selecting-end-date="selectingEndDate"
|
||||||
:hovered-end-date="hoveredEndDate"
|
:hovered-end-date="hoveredEndDate"
|
||||||
@updateHoveredEndDate="hoveredEndDate = $event"
|
@update-hovered-end-date="hoveredEndDate = $event"
|
||||||
@selectDate="selectDate"
|
@select-date="selectDate"
|
||||||
@setView="setViewMode"
|
@set-view="setViewMode"
|
||||||
@prev="moveCalendar(calendar, 'prev')"
|
@prev="moveCalendar(calendar, 'prev')"
|
||||||
@next="moveCalendar(calendar, 'next')"
|
@next="moveCalendar(calendar, 'next')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['clear', 'clear']);
|
const emit = defineEmits(['clear', 'change']);
|
||||||
|
|
||||||
const onClickClear = () => {
|
const onClickClear = () => {
|
||||||
emit('clear');
|
emit('clear');
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const selectMonth = index => {
|
|||||||
MONTH
|
MONTH
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@setView="setViewMode"
|
@set-view="setViewMode"
|
||||||
@prev="onClickPrev"
|
@prev="onClickPrev"
|
||||||
@next="onClickNext"
|
@next="onClickNext"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const dayClasses = day => ({
|
|||||||
"
|
"
|
||||||
@prev="onClickPrev"
|
@prev="onClickPrev"
|
||||||
@next="onClickNext"
|
@next="onClickNext"
|
||||||
@setView="setViewMode"
|
@set-view="setViewMode"
|
||||||
/>
|
/>
|
||||||
<CalendarWeekLabel />
|
<CalendarWeekLabel />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['change'],
|
||||||
methods: {
|
methods: {
|
||||||
handleChange(value) {
|
handleChange(value) {
|
||||||
this.$emit('change', value);
|
this.$emit('change', value);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default {
|
|||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['change'],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleChange(value) {
|
handleChange(value) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['onSearch', 'select']);
|
const emit = defineEmits(['onSearch', 'select', 'removeFilter']);
|
||||||
|
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
|
|
||||||
@@ -84,10 +84,10 @@ const shouldShowEmptyState = computed(() => {
|
|||||||
<slot name="search">
|
<slot name="search">
|
||||||
<DropdownSearch
|
<DropdownSearch
|
||||||
v-if="enableSearch"
|
v-if="enableSearch"
|
||||||
:input-value="searchTerm"
|
v-model="searchTerm"
|
||||||
:input-placeholder="inputPlaceholder"
|
:input-placeholder="inputPlaceholder"
|
||||||
:show-clear-filter="showClearFilter"
|
:show-clear-filter="showClearFilter"
|
||||||
@input="onSearch"
|
@update:model-value="onSearch"
|
||||||
@remove="$emit('removeFilter')"
|
@remove="$emit('removeFilter')"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits } from 'vue';
|
import { defineEmits, defineModel } from 'vue';
|
||||||
defineProps({
|
defineProps({
|
||||||
inputValue: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
inputPlaceholder: {
|
inputPlaceholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@@ -15,7 +11,12 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['input', 'remove']);
|
const emit = defineEmits(['remove']);
|
||||||
|
|
||||||
|
const value = defineModel({
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -29,16 +30,15 @@ const emit = defineEmits(['input', 'remove']);
|
|||||||
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
|
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
|
v-model="value"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
|
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>
|
</div>
|
||||||
<!-- Clear filter button -->
|
<!-- Clear filter button -->
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="!inputValue && showClearFilter"
|
v-if="!modelValue && showClearFilter"
|
||||||
size="small"
|
size="small"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['remove'],
|
||||||
computed: {
|
computed: {
|
||||||
textColor() {
|
textColor() {
|
||||||
if (this.variant === 'smooth') return '';
|
if (this.variant === 'smooth') return '';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export default {
|
|||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
size: { type: String, default: '' },
|
size: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue', 'input'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.$emit('update:modelValue', !this.modelValue);
|
this.$emit('update:modelValue', !this.modelValue);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default {
|
|||||||
AICTAModal,
|
AICTAModal,
|
||||||
AIAssistanceCTAButton,
|
AIAssistanceCTAButton,
|
||||||
},
|
},
|
||||||
|
emits: ['replaceText'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
@@ -137,19 +138,19 @@ export default {
|
|||||||
@click="openAIAssist"
|
@click="openAIAssist"
|
||||||
/>
|
/>
|
||||||
<woot-modal
|
<woot-modal
|
||||||
:show.sync="showAIAssistanceModal"
|
v-model:show="showAIAssistanceModal"
|
||||||
:on-close="hideAIAssistanceModal"
|
:on-close="hideAIAssistanceModal"
|
||||||
>
|
>
|
||||||
<AIAssistanceModal
|
<AIAssistanceModal
|
||||||
:ai-option="aiOption"
|
:ai-option="aiOption"
|
||||||
@applyText="insertText"
|
@apply-text="insertText"
|
||||||
@close="hideAIAssistanceModal"
|
@close="hideAIAssistanceModal"
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
|
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
|
||||||
<AIAssistanceCTAButton @click="openAICta" />
|
<AIAssistanceCTAButton @click="openAICta" />
|
||||||
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
|
<woot-modal v-model:show="showAICtaModal" :on-close="hideAICtaModal">
|
||||||
<AICTAModal @close="hideAICtaModal" />
|
<AICTAModal @close="hideAICtaModal" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['open'],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.$emit('open');
|
this.$emit('open');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['close', 'applyText'],
|
||||||
setup() {
|
setup() {
|
||||||
const { formatMessage } = useMessageFormatter();
|
const { formatMessage } = useMessageFormatter();
|
||||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { useAI } from 'dashboard/composables/useAI';
|
|||||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const { updateUISettings } = useUISettings();
|
const { updateUISettings } = useUISettings();
|
||||||
const { recordAnalytics } = useAI();
|
const { recordAnalytics } = useAI();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
// The value types are dynamic, hence prop validation removed to work with our action schema
|
props: {
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
teams: { type: Array, required: true },
|
||||||
props: ['teams', 'value'],
|
modelValue: { type: Object, required: true },
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedTeams: [],
|
selectedTeams: [],
|
||||||
@@ -10,13 +12,13 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const { team_ids: teamIds } = this.value;
|
const { team_ids: teamIds } = this.modelValue;
|
||||||
this.selectedTeams = teamIds;
|
this.selectedTeams = teamIds;
|
||||||
this.message = this.value.message;
|
this.message = this.modelValue.message;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateValue() {
|
updateValue() {
|
||||||
this.$emit('input', {
|
this.$emit('update:modelValue', {
|
||||||
team_ids: this.selectedTeams.map(team => team.id),
|
team_ids: this.selectedTeams.map(team => team.id),
|
||||||
message: this.message,
|
message: this.message,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
uploadState: 'idle',
|
uploadState: 'idle',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['channelItemClick'],
|
||||||
computed: {
|
computed: {
|
||||||
hasFbConfigured() {
|
hasFbConfigured() {
|
||||||
return window.chatwootConfig?.fbAppId;
|
return window.chatwootConfig?.fbAppId;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isPickerOpen: false,
|
isPickerOpen: false,
|
||||||
@@ -45,7 +46,7 @@ export default {
|
|||||||
disable-alpha
|
disable-alpha
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
class="colorpicker--chrome"
|
class="colorpicker--chrome"
|
||||||
@update:modelValue="updateColor"
|
@update:model-value="updateColor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
|
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isFeatureEnabled">
|
<div v-if="isFeatureEnabled">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue', 'removeFilter', 'resetFilter'],
|
||||||
computed: {
|
computed: {
|
||||||
attributeKey: {
|
attributeKey: {
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
inbox: {
|
inbox: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const props = defineProps({
|
|||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['pageChange']);
|
||||||
const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize));
|
const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize));
|
||||||
const firstIndex = computed(() => props.pageSize * (props.currentPage - 1) + 1);
|
const firstIndex = computed(() => props.pageSize * (props.currentPage - 1) + 1);
|
||||||
const lastIndex = computed(() =>
|
const lastIndex = computed(() =>
|
||||||
@@ -26,6 +28,7 @@ const isFooterVisible = computed(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<footer
|
<footer
|
||||||
v-if="isFooterVisible"
|
v-if="isFooterVisible"
|
||||||
@@ -42,7 +45,7 @@ const isFooterVisible = computed(
|
|||||||
:total-pages="totalPages"
|
:total-pages="totalPages"
|
||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
@pageChange="$emit('pageChange', $event)"
|
@page-change="emit('pageChange', $event)"
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="isVideoIntegrationEnabled"
|
v-if="isVideoIntegrationEnabled"
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['recorderProgressChanged', 'finishRecord']);
|
const emit = defineEmits([
|
||||||
|
'recorderProgressChanged',
|
||||||
|
'finishRecord',
|
||||||
|
'pause',
|
||||||
|
'play',
|
||||||
|
]);
|
||||||
|
|
||||||
const waveformContainer = ref(null);
|
const waveformContainer = ref(null);
|
||||||
const wavesurfer = 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];
|
record.value = wavesurfer.value.plugins[0];
|
||||||
|
|
||||||
wavesurfer.value.on('finish', () => {
|
wavesurfer.value.on('finish', () => {
|
||||||
@@ -106,11 +114,9 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({ playPause, stopRecording });
|
defineExpose({ playPause, stopRecording, record });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div ref="waveformContainer" class="w-full p-1" />
|
||||||
<div ref="waveformContainer" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const editorMenuOptions = computed(() => {
|
|||||||
|
|
||||||
function createSuggestionPlugin({
|
function createSuggestionPlugin({
|
||||||
trigger,
|
trigger,
|
||||||
minChars = 1,
|
minChars = 0,
|
||||||
showMenu,
|
showMenu,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
isAllowed = () => true,
|
isAllowed = () => true,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default {
|
|||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
},
|
},
|
||||||
|
emits: ['blur', 'input', 'update:modelValue', 'keyup', 'focus', 'keydown'],
|
||||||
setup() {
|
setup() {
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
@@ -60,17 +61,9 @@ export default {
|
|||||||
plugins: [imagePastePlugin(this.handleImageUpload)],
|
plugins: [imagePastePlugin(this.handleImageUpload)],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
contentFromEditor() {
|
|
||||||
if (editorView) {
|
|
||||||
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
modelValue(newValue = '') {
|
modelValue(newValue = '') {
|
||||||
if (newValue !== this.contentFromEditor) {
|
if (newValue !== this.contentFromEditor()) {
|
||||||
this.reloadState();
|
this.reloadState();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -95,6 +88,12 @@ export default {
|
|||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contentFromEditor() {
|
||||||
|
if (editorView) {
|
||||||
|
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
this.$refs.imageUploadInput.click();
|
this.$refs.imageUploadInput.click();
|
||||||
},
|
},
|
||||||
@@ -212,8 +211,8 @@ export default {
|
|||||||
editorView.focus();
|
editorView.focus();
|
||||||
},
|
},
|
||||||
emitOnChange() {
|
emitOnChange() {
|
||||||
this.$emit('update:modelValue', this.contentFromEditor);
|
this.$emit('update:modelValue', this.contentFromEditor());
|
||||||
this.$emit('input', this.contentFromEditor);
|
this.$emit('input', this.contentFromEditor());
|
||||||
},
|
},
|
||||||
onKeyup() {
|
onKeyup() {
|
||||||
this.$emit('keyup');
|
this.$emit('keyup');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { ref } from 'vue';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
@@ -26,7 +27,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onSend: {
|
onSend: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
sendButtonText: {
|
sendButtonText: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -34,7 +35,7 @@ export default {
|
|||||||
},
|
},
|
||||||
recordingAudioDurationText: {
|
recordingAudioDurationText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '00:00',
|
||||||
},
|
},
|
||||||
// inbox prop is used in /mixins/inboxMixin,
|
// inbox prop is used in /mixins/inboxMixin,
|
||||||
// remove this props when refactoring to composable if not needed
|
// remove this props when refactoring to composable if not needed
|
||||||
@@ -53,19 +54,19 @@ export default {
|
|||||||
},
|
},
|
||||||
onFileUpload: {
|
onFileUpload: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
toggleEmojiPicker: {
|
toggleEmojiPicker: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
toggleAudioRecorder: {
|
toggleAudioRecorder: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
toggleAudioRecorderPlayPause: {
|
toggleAudioRecorderPlayPause: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => {},
|
||||||
},
|
},
|
||||||
isRecordingAudio: {
|
isRecordingAudio: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -112,10 +113,18 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: [
|
||||||
|
'replaceText',
|
||||||
|
'toggleInsertArticle',
|
||||||
|
'toggleEditor',
|
||||||
|
'selectWhatsappTemplate',
|
||||||
|
],
|
||||||
setup() {
|
setup() {
|
||||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||||
useUISettings();
|
useUISettings();
|
||||||
|
|
||||||
|
const uploadRef = ref(false);
|
||||||
|
|
||||||
const keyboardEvents = {
|
const keyboardEvents = {
|
||||||
'Alt+KeyA': {
|
'Alt+KeyA': {
|
||||||
action: () => {
|
action: () => {
|
||||||
@@ -137,6 +146,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
setSignatureFlagForInbox,
|
setSignatureFlagForInbox,
|
||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
|
uploadRef,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -166,9 +176,9 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Disable audio recorder for safari browser as recording is not supported
|
// Disable audio recorder for safari browser as recording is not supported
|
||||||
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
// const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
||||||
navigator.userAgent
|
// navigator.userAgent
|
||||||
);
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.isFeatureEnabledonAccount(
|
this.isFeatureEnabledonAccount(
|
||||||
@@ -249,13 +259,13 @@ export default {
|
|||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||||
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||||
icon="emoji"
|
icon="emoji"
|
||||||
emoji="😊"
|
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
@click="toggleEmojiPicker"
|
@click="toggleEmojiPicker"
|
||||||
/>
|
/>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
|
ref="uploadRef"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||||
input-id="conversationAttachment"
|
input-id="conversationAttachment"
|
||||||
:size="4096 * 4096"
|
:size="4096 * 4096"
|
||||||
@@ -274,7 +284,6 @@ export default {
|
|||||||
class-names="button--upload"
|
class-names="button--upload"
|
||||||
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||||
icon="attach"
|
icon="attach"
|
||||||
emoji="📎"
|
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -284,7 +293,6 @@ export default {
|
|||||||
v-if="showAudioRecorderButton"
|
v-if="showAudioRecorderButton"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||||
emoji="🎤"
|
|
||||||
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
|
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -294,7 +302,6 @@ export default {
|
|||||||
v-if="showEditorToggle"
|
v-if="showEditorToggle"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||||
icon="quote"
|
icon="quote"
|
||||||
emoji="🖊️"
|
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -303,7 +310,6 @@ export default {
|
|||||||
<woot-button
|
<woot-button
|
||||||
v-if="showAudioPlayStopButton"
|
v-if="showAudioPlayStopButton"
|
||||||
:icon="audioRecorderPlayStopIcon"
|
:icon="audioRecorderPlayStopIcon"
|
||||||
emoji="🎤"
|
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -340,11 +346,11 @@ export default {
|
|||||||
:conversation-id="conversationId"
|
:conversation-id="conversationId"
|
||||||
:is-private-note="isOnPrivateNote"
|
:is-private-note="isOnPrivateNote"
|
||||||
:message="message"
|
:message="message"
|
||||||
@replaceText="replaceText"
|
@replace-text="replaceText"
|
||||||
/>
|
/>
|
||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<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"
|
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" />
|
<fluent-icon icon="cloud-backup" size="40" />
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['setReplyMode', 'togglePopout'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const setReplyMode = mode => {
|
const setReplyMode = mode => {
|
||||||
emit('setReplyMode', mode);
|
emit('setReplyMode', mode);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ onMounted(() => {
|
|||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
type="emoji"
|
type="emoji"
|
||||||
:items="items"
|
:items="items"
|
||||||
@mentionSelect="handleMentionClick"
|
@mention-select="handleMentionClick"
|
||||||
>
|
>
|
||||||
<template #default="{ item, selected }">
|
<template #default="{ item, selected }">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import lamejs from '@breezystack/lamejs';
|
import lamejs from '@breezystack/lamejs';
|
||||||
|
|
||||||
const writeString = (view, offset, string) => {
|
const writeString = (view, offset, string) => {
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < string.length; i++) {
|
for (let i = 0; i < string.length; i++) {
|
||||||
view.setUint8(offset + i, string.charCodeAt(i));
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,9 @@ const bufferToWav = async (buffer, numChannels, sampleRate) => {
|
|||||||
|
|
||||||
// WAV Data
|
// WAV Data
|
||||||
const offset = 44;
|
const offset = 44;
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let channel = 0; channel < numChannels; channel++) {
|
for (let channel = 0; channel < numChannels; channel++) {
|
||||||
const sample = Math.max(
|
const sample = Math.max(
|
||||||
-1,
|
-1,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['replace'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
cannedMessages: 'getCannedResponses',
|
cannedMessages: 'getCannedResponses',
|
||||||
@@ -46,6 +47,6 @@ export default {
|
|||||||
<MentionBox
|
<MentionBox
|
||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
:items="items"
|
:items="items"
|
||||||
@mentionSelect="handleMentionClick"
|
@mention-select="handleMentionClick"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+3
-2
@@ -37,6 +37,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['applyFilter', 'updateFolder'],
|
||||||
setup() {
|
setup() {
|
||||||
const { setFilterAttributes } = useFilter({
|
const { setFilterAttributes } = useFilter({
|
||||||
filteri18nKey: 'FILTER',
|
filteri18nKey: 'FILTER',
|
||||||
@@ -363,8 +364,8 @@ export default {
|
|||||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
@reset-filter="resetFilter(i, appliedFilters[i])"
|
||||||
@removeFilter="removeFilter(i)"
|
@remove-filter="removeFilter(i)"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<woot-button
|
<woot-button
|
||||||
|
|||||||
+3
-2
@@ -27,6 +27,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FilterItem,
|
FilterItem,
|
||||||
},
|
},
|
||||||
|
emits: ['changeFilter'],
|
||||||
setup() {
|
setup() {
|
||||||
const { updateUISettings } = useUISettings();
|
const { updateUISettings } = useUISettings();
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ export default {
|
|||||||
:selected-value="chatStatus"
|
:selected-value="chatStatus"
|
||||||
:items="chatStatusItems"
|
:items="chatStatusItems"
|
||||||
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
|
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
|
||||||
@onChangeFilter="onChangeFilter"
|
@on-change-filter="onChangeFilter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between last:mt-4">
|
<div class="flex items-center justify-between last:mt-4">
|
||||||
@@ -119,7 +120,7 @@ export default {
|
|||||||
:selected-value="sortFilter"
|
:selected-value="sortFilter"
|
||||||
:items="chatSortItems"
|
:items="chatSortItems"
|
||||||
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
|
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
|
||||||
@onChangeFilter="onChangeFilter"
|
@on-change-filter="onChangeFilter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['contactPanelToggle'],
|
||||||
data() {
|
data() {
|
||||||
return { activeIndex: 0 };
|
return { activeIndex: 0 };
|
||||||
},
|
},
|
||||||
@@ -103,7 +104,7 @@ export default {
|
|||||||
:is-inbox-view="isInboxView"
|
:is-inbox-view="isInboxView"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
:show-back-button="isOnExpandedLayout && !isInboxView"
|
:show-back-button="isOnExpandedLayout && !isInboxView"
|
||||||
@contactPanelToggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
<woot-tabs
|
<woot-tabs
|
||||||
v-if="dashboardApps.length && currentChat.id"
|
v-if="dashboardApps.length && currentChat.id"
|
||||||
@@ -128,7 +129,7 @@ export default {
|
|||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
:is-inbox-view="isInboxView"
|
:is-inbox-view="isInboxView"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
@contactPanelToggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
v-if="!currentChat.id && !isInboxView"
|
v-if="!currentChat.id && !isInboxView"
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: [
|
||||||
|
'contextMenuToggle',
|
||||||
|
'assignAgent',
|
||||||
|
'assignLabel',
|
||||||
|
'assignTeam',
|
||||||
|
'markAsUnread',
|
||||||
|
'assignPriority',
|
||||||
|
'updateConversationStatus',
|
||||||
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hovered: false,
|
hovered: false,
|
||||||
@@ -336,12 +345,12 @@ export default {
|
|||||||
:priority="chat.priority"
|
:priority="chat.priority"
|
||||||
:chat-id="chat.id"
|
:chat-id="chat.id"
|
||||||
:has-unread-messages="hasUnread"
|
:has-unread-messages="hasUnread"
|
||||||
@updateConversation="onUpdateConversation"
|
@update-conversation="onUpdateConversation"
|
||||||
@assignAgent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@assignLabel="onAssignLabel"
|
@assign-label="onAssignLabel"
|
||||||
@assignTeam="onAssignTeam"
|
@assign-team="onAssignTeam"
|
||||||
@markAsUnread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@assignPriority="assignPriority"
|
@assign-priority="assignPriority"
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['contactPanelToggle'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const keyboardEvents = {
|
const keyboardEvents = {
|
||||||
'Alt+KeyO': {
|
'Alt+KeyO': {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, minLength, email } from '@vuelidate/validators';
|
import { required, minLength, email } from '@vuelidate/validators';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
@@ -13,6 +14,7 @@ export default {
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['cancel', 'update:show'],
|
||||||
setup() {
|
setup() {
|
||||||
return { v$: useVuelidate() };
|
return { v$: useVuelidate() };
|
||||||
},
|
},
|
||||||
@@ -31,6 +33,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
localShow: {
|
||||||
|
get() {
|
||||||
|
return this.show;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:show', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
sentToOtherEmailAddress() {
|
sentToOtherEmailAddress() {
|
||||||
return this.selectedType === 'other_email_address';
|
return this.selectedType === 'other_email_address';
|
||||||
},
|
},
|
||||||
@@ -80,9 +90,8 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
|
||||||
<template>
|
<template>
|
||||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
<woot-modal v-model:show="localShow" :on-close="onCancel">
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
<div class="flex flex-col h-auto overflow-auto">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
|
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['onChangeFilter'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeValue: this.selectedValue,
|
activeValue: this.selectedValue,
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
v-if="shouldRenderMessage"
|
v-if="shouldRenderMessage"
|
||||||
@@ -579,7 +580,7 @@ export default {
|
|||||||
:message="data"
|
:message="data"
|
||||||
@open="openContextMenu"
|
@open="openContextMenu"
|
||||||
@close="closeContextMenu"
|
@close="closeContextMenu"
|
||||||
@replyTo="handleReplyTo"
|
@reply-to="handleReplyTo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['contactPanelToggle'],
|
||||||
setup() {
|
setup() {
|
||||||
const isPopOutReplyBox = ref(false);
|
const isPopOutReplyBox = ref(false);
|
||||||
const { isEnterprise } = useConfig();
|
const { isEnterprise } = useConfig();
|
||||||
@@ -477,6 +478,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<ul class="conversation-panel">
|
<ul class="conversation-panel">
|
||||||
<transition name="slide-up">
|
<transition name="slide-up">
|
||||||
|
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||||
<li class="min-h-[4rem]">
|
<li class="min-h-[4rem]">
|
||||||
<span v-if="shouldShowSpinner" class="spinner message" />
|
<span v-if="shouldShowSpinner" class="spinner message" />
|
||||||
</li>
|
</li>
|
||||||
@@ -547,9 +549,9 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ReplyBox
|
<ReplyBox
|
||||||
|
v-model:popout-reply-box="isPopOutReplyBox"
|
||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:popout-reply-box.sync="isPopOutReplyBox"
|
@toggle-popout="showPopOutReplyBox"
|
||||||
@togglePopout="showPopOutReplyBox"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
|
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||||
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||||
},
|
},
|
||||||
destroyed() {
|
unmounted() {
|
||||||
emitter.off(CMD_MUTE_CONVERSATION, this.mute);
|
emitter.off(CMD_MUTE_CONVERSATION, this.mute);
|
||||||
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
|
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||||
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-if="priority"
|
v-if="priority"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { defineAsyncComponent } from 'vue';
|
// [TODO] The popout events are needlessly complex and should be simplified
|
||||||
|
import { defineAsyncComponent, defineModel } from 'vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
@@ -18,7 +19,7 @@ import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
|||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
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 { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import {
|
import {
|
||||||
@@ -49,28 +50,23 @@ const EmojiInput = defineAsyncComponent(
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmojiInput,
|
|
||||||
CannedResponse,
|
|
||||||
ReplyToMessage,
|
|
||||||
ResizableTextArea,
|
|
||||||
AttachmentPreview,
|
|
||||||
ReplyTopPanel,
|
|
||||||
ReplyEmailHead,
|
|
||||||
ReplyBottomPanel,
|
|
||||||
WootMessageEditor,
|
|
||||||
WootAudioRecorder,
|
|
||||||
Banner,
|
|
||||||
WhatsappTemplates,
|
|
||||||
MessageSignatureMissingAlert,
|
|
||||||
ArticleSearchPopover,
|
ArticleSearchPopover,
|
||||||
|
AttachmentPreview,
|
||||||
|
AudioRecorder,
|
||||||
|
Banner,
|
||||||
|
CannedResponse,
|
||||||
|
EmojiInput,
|
||||||
|
MessageSignatureMissingAlert,
|
||||||
|
ReplyBottomPanel,
|
||||||
|
ReplyEmailHead,
|
||||||
|
ReplyToMessage,
|
||||||
|
ReplyTopPanel,
|
||||||
|
ResizableTextArea,
|
||||||
|
WhatsappTemplates,
|
||||||
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
emits: ['update:popoutReplyBox', 'togglePopout'],
|
||||||
popoutReplyBox: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
setup() {
|
||||||
const {
|
const {
|
||||||
uiSettings,
|
uiSettings,
|
||||||
@@ -79,8 +75,14 @@ export default {
|
|||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
} = useUISettings();
|
} = useUISettings();
|
||||||
|
|
||||||
|
const popoutReplyBox = defineModel('popoutReplyBox', {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uiSettings,
|
uiSettings,
|
||||||
|
popoutReplyBox,
|
||||||
updateUISettings,
|
updateUISettings,
|
||||||
isEditorHotKeyEnabled,
|
isEditorHotKeyEnabled,
|
||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
@@ -113,6 +115,7 @@ export default {
|
|||||||
showVariablesMenu: false,
|
showVariablesMenu: false,
|
||||||
newConversationModalActive: false,
|
newConversationModalActive: false,
|
||||||
showArticleSearchPopover: false,
|
showArticleSearchPopover: false,
|
||||||
|
hasRecordedAudio: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -277,12 +280,6 @@ export default {
|
|||||||
hasAttachments() {
|
hasAttachments() {
|
||||||
return this.attachedFiles.length;
|
return this.attachedFiles.length;
|
||||||
},
|
},
|
||||||
hasRecordedAudio() {
|
|
||||||
return (
|
|
||||||
this.$refs.audioRecorderInput &&
|
|
||||||
this.$refs.audioRecorderInput.hasAudio()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isRichEditorEnabled() {
|
isRichEditorEnabled() {
|
||||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||||
},
|
},
|
||||||
@@ -362,7 +359,7 @@ export default {
|
|||||||
return AUDIO_FORMATS.MP3;
|
return AUDIO_FORMATS.MP3;
|
||||||
}
|
}
|
||||||
if (this.isAPIInbox) {
|
if (this.isAPIInbox) {
|
||||||
return AUDIO_FORMATS.OGG;
|
return AUDIO_FORMATS.MP3;
|
||||||
}
|
}
|
||||||
return AUDIO_FORMATS.WAV;
|
return AUDIO_FORMATS.WAV;
|
||||||
},
|
},
|
||||||
@@ -463,12 +460,10 @@ export default {
|
|||||||
this.onNewConversationModalActive
|
this.onNewConversationModalActive
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
destroyed() {
|
unmounted() {
|
||||||
document.removeEventListener('paste', this.onPaste);
|
document.removeEventListener('paste', this.onPaste);
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
emitter.off(
|
emitter.off(
|
||||||
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||||
this.onNewConversationModalActive
|
this.onNewConversationModalActive
|
||||||
@@ -803,19 +798,14 @@ export default {
|
|||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
this.resetReplyToMessage();
|
this.resetReplyToMessage();
|
||||||
|
this.resetAudioRecorderInput();
|
||||||
},
|
},
|
||||||
clearEmailField() {
|
clearEmailField() {
|
||||||
this.ccEmails = '';
|
this.ccEmails = '';
|
||||||
this.bccEmails = '';
|
this.bccEmails = '';
|
||||||
this.toEmails = '';
|
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() {
|
toggleEmojiPicker() {
|
||||||
this.showEmojiPicker = !this.showEmojiPicker;
|
this.showEmojiPicker = !this.showEmojiPicker;
|
||||||
},
|
},
|
||||||
@@ -823,17 +813,18 @@ export default {
|
|||||||
this.isRecordingAudio = !this.isRecordingAudio;
|
this.isRecordingAudio = !this.isRecordingAudio;
|
||||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||||
if (!this.isRecordingAudio) {
|
if (!this.isRecordingAudio) {
|
||||||
this.clearRecorder();
|
this.resetAudioRecorderInput();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleAudioRecorderPlayPause() {
|
toggleAudioRecorderPlayPause() {
|
||||||
if (this.isRecordingAudio) {
|
if (!this.isRecordingAudio) {
|
||||||
if (!this.isRecorderAudioStopped) {
|
return;
|
||||||
this.isRecorderAudioStopped = true;
|
}
|
||||||
this.$refs.audioRecorderInput.stopRecording();
|
if (!this.isRecorderAudioStopped) {
|
||||||
} else if (this.isRecorderAudioStopped) {
|
this.isRecorderAudioStopped = true;
|
||||||
this.$refs.audioRecorderInput.playPause();
|
this.$refs.audioRecorderInput.stopRecording();
|
||||||
}
|
} else if (this.isRecorderAudioStopped) {
|
||||||
|
this.$refs.audioRecorderInput.playPause();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideEmojiPicker() {
|
hideEmojiPicker() {
|
||||||
@@ -860,13 +851,9 @@ export default {
|
|||||||
onRecordProgressChanged(duration) {
|
onRecordProgressChanged(duration) {
|
||||||
this.recordingAudioDurationText = duration;
|
this.recordingAudioDurationText = duration;
|
||||||
},
|
},
|
||||||
onStateRecorderChanged(state) {
|
|
||||||
this.recordingAudioState = state;
|
|
||||||
if (state && 'notallowederror'.includes(state)) {
|
|
||||||
this.toggleAudioRecorder();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFinishRecorder(file) {
|
onFinishRecorder(file) {
|
||||||
|
this.recordingAudioState = 'stopped';
|
||||||
|
this.hasRecordedAudio = true;
|
||||||
// Added a new key isRecordedAudio to the file to find it's and recorded audio
|
// 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
|
// Because to filter and show only non recorded audio and other attachments
|
||||||
const autoRecordedFile = {
|
const autoRecordedFile = {
|
||||||
@@ -1067,6 +1054,16 @@ export default {
|
|||||||
toggleInsertArticle() {
|
toggleInsertArticle() {
|
||||||
this.showArticleSearchPopover = !this.showArticleSearchPopover;
|
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>
|
</script>
|
||||||
@@ -1081,15 +1078,15 @@ export default {
|
|||||||
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
||||||
has-action-button
|
has-action-button
|
||||||
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
||||||
@primaryAction="onClickSelfAssign"
|
@primary-action="onClickSelfAssign"
|
||||||
/>
|
/>
|
||||||
<ReplyTopPanel
|
<ReplyTopPanel
|
||||||
:mode="replyType"
|
:mode="replyType"
|
||||||
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
||||||
:characters-remaining="charactersRemaining"
|
:characters-remaining="charactersRemaining"
|
||||||
:popout-reply-box="popoutReplyBox"
|
:popout-reply-box="popoutReplyBox"
|
||||||
@setReplyMode="setReplyMode"
|
@set-reply-mode="setReplyMode"
|
||||||
@togglePopout="$emit('togglePopout')"
|
@toggle-popout="$emit('togglePopout')"
|
||||||
/>
|
/>
|
||||||
<ArticleSearchPopover
|
<ArticleSearchPopover
|
||||||
v-if="showArticleSearchPopover && connectedPortalSlug"
|
v-if="showArticleSearchPopover && connectedPortalSlug"
|
||||||
@@ -1122,13 +1119,14 @@ export default {
|
|||||||
v-model:bcc-emails="bccEmails"
|
v-model:bcc-emails="bccEmails"
|
||||||
v-model:to-emails="toEmails"
|
v-model:to-emails="toEmails"
|
||||||
/>
|
/>
|
||||||
<WootAudioRecorder
|
<AudioRecorder
|
||||||
v-if="showAudioRecorderEditor"
|
v-if="showAudioRecorderEditor"
|
||||||
ref="audioRecorderInput"
|
ref="audioRecorderInput"
|
||||||
:audio-record-format="audioRecordFormat"
|
:audio-record-format="audioRecordFormat"
|
||||||
@recorderProgressChanged="onRecordProgressChanged"
|
@recorder-progress-changed="onRecordProgressChanged"
|
||||||
@stateRecorderChanged="onStateRecorderChanged"
|
@finish-record="onFinishRecorder"
|
||||||
@finishRecord="onFinishRecorder"
|
@play="recordingAudioState = 'playing'"
|
||||||
|
@pause="recordingAudioState = 'paused'"
|
||||||
/>
|
/>
|
||||||
<ResizableTextArea
|
<ResizableTextArea
|
||||||
v-else-if="!showRichContentEditor"
|
v-else-if="!showRichContentEditor"
|
||||||
@@ -1140,8 +1138,8 @@ export default {
|
|||||||
:signature="signatureToApply"
|
:signature="signatureToApply"
|
||||||
allow-signature
|
allow-signature
|
||||||
:send-with-signature="sendWithSignature"
|
:send-with-signature="sendWithSignature"
|
||||||
@typingOff="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typingOn="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
@@ -1159,21 +1157,25 @@ export default {
|
|||||||
:signature="signatureToApply"
|
:signature="signatureToApply"
|
||||||
allow-signature
|
allow-signature
|
||||||
:channel-type="channelType"
|
:channel-type="channelType"
|
||||||
@typingOff="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typingOn="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@toggleUserMention="toggleUserMention"
|
@toggle-user-mention="toggleUserMention"
|
||||||
@toggleCannedMenu="toggleCannedMenu"
|
@toggle-canned-menu="toggleCannedMenu"
|
||||||
@toggleVariablesMenu="toggleVariablesMenu"
|
@toggle-variables-menu="toggleVariablesMenu"
|
||||||
@clearSelection="clearEditorSelection"
|
@clear-selection="clearEditorSelection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
<div
|
||||||
|
v-if="hasAttachments && !showAudioRecorderEditor"
|
||||||
|
class="attachment-preview-box"
|
||||||
|
@paste="onPaste"
|
||||||
|
>
|
||||||
<AttachmentPreview
|
<AttachmentPreview
|
||||||
class="flex-col mt-4"
|
class="flex-col mt-4"
|
||||||
:attachments="attachedFiles"
|
:attachments="attachedFiles"
|
||||||
@removeAttachment="removeAttachment"
|
@remove-attachment="removeAttachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MessageSignatureMissingAlert
|
<MessageSignatureMissingAlert
|
||||||
@@ -1203,16 +1205,16 @@ export default {
|
|||||||
:message="message"
|
:message="message"
|
||||||
:portal-slug="connectedPortalSlug"
|
:portal-slug="connectedPortalSlug"
|
||||||
:new-conversation-modal-active="newConversationModalActive"
|
:new-conversation-modal-active="newConversationModalActive"
|
||||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
@select-whatsapp-template="openWhatsappTemplateModal"
|
||||||
@toggleEditor="toggleRichContentEditor"
|
@toggle-editor="toggleRichContentEditor"
|
||||||
@replaceText="replaceText"
|
@replace-text="replaceText"
|
||||||
@toggleInsertArticle="toggleInsertArticle"
|
@toggle-insert-article="toggleInsertArticle"
|
||||||
/>
|
/>
|
||||||
<WhatsappTemplates
|
<WhatsappTemplates
|
||||||
:inbox-id="inbox.id"
|
:inbox-id="inbox.id"
|
||||||
:show="showWhatsAppTemplatesModal"
|
:show="showWhatsAppTemplatesModal"
|
||||||
@close="hideWhatsappTemplatesModal"
|
@close="hideWhatsappTemplatesModal"
|
||||||
@onSend="onSendWhatsAppReply"
|
@on-send="onSendWhatsAppReply"
|
||||||
@cancel="hideWhatsappTemplatesModal"
|
@cancel="hideWhatsappTemplatesModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['update:bccEmails', 'update:ccEmails', 'update:toEmails'],
|
||||||
setup() {
|
setup() {
|
||||||
return { v$: useVuelidate() };
|
return { v$: useVuelidate() };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['dismiss'],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['selectVariable'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
customAttributes: 'attributes/getAttributes',
|
customAttributes: 'attributes/getAttributes',
|
||||||
@@ -62,7 +63,7 @@ export default {
|
|||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
type="variable"
|
type="variable"
|
||||||
:items="items"
|
:items="items"
|
||||||
@mentionSelect="handleVariableClick"
|
@mention-select="handleVariableClick"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+17
-9
@@ -7,21 +7,30 @@ export default {
|
|||||||
TemplateParser,
|
TemplateParser,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
inboxId: {
|
inboxId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
emits: ['onSend', 'cancel', 'update:show'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedWaTemplate: null,
|
selectedWaTemplate: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
localShow: {
|
||||||
|
get() {
|
||||||
|
return this.show;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:show', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
modalHeaderContent() {
|
modalHeaderContent() {
|
||||||
return this.selectedWaTemplate
|
return this.selectedWaTemplate
|
||||||
? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', {
|
? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', {
|
||||||
@@ -47,9 +56,8 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
|
||||||
<template>
|
<template>
|
||||||
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
|
<woot-modal v-model:show="localShow" :on-close="onClose" size="modal-big">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
:header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')"
|
:header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')"
|
||||||
:header-content="modalHeaderContent"
|
:header-content="modalHeaderContent"
|
||||||
@@ -58,13 +66,13 @@ export default {
|
|||||||
<TemplatesPicker
|
<TemplatesPicker
|
||||||
v-if="!selectedWaTemplate"
|
v-if="!selectedWaTemplate"
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
@onSelect="pickTemplate"
|
@on-select="pickTemplate"
|
||||||
/>
|
/>
|
||||||
<TemplateParser
|
<TemplateParser
|
||||||
v-else
|
v-else
|
||||||
:template="selectedWaTemplate"
|
:template="selectedWaTemplate"
|
||||||
@resetTemplate="onResetTemplate"
|
@reset-template="onResetTemplate"
|
||||||
@sendMessage="onSendMessage"
|
@send-message="onSendMessage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ export default {
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['sendMessage', 'resetTemplate'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const processVariable = str => {
|
const processVariable = str => {
|
||||||
return str.replace(/{{|}}/g, '');
|
return str.replace(/{{|}}/g, '');
|
||||||
|
|||||||
+1
@@ -9,6 +9,7 @@ export default {
|
|||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['onSelect'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: '',
|
query: '',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['error'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -26,7 +27,7 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div class="image message-text__wrap">
|
<div class="image message-text__wrap">
|
||||||
<img :src="url" @click="onClick" @error="$emit('error')" />
|
<img :src="url" @click="onClick" @error="$emit('error')" />
|
||||||
<woot-modal full-width :show.sync="show" :on-close="onClose">
|
<woot-modal v-model:show="show" full-width :on-close="onClose">
|
||||||
<img :src="url" class="modal-image skip-context-menu" />
|
<img :src="url" class="modal-image skip-context-menu" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['error'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -116,7 +117,7 @@ export default {
|
|||||||
</audio>
|
</audio>
|
||||||
<GalleryView
|
<GalleryView
|
||||||
v-if="show"
|
v-if="show"
|
||||||
:show.sync="show"
|
v-model:show="show"
|
||||||
:attachment="attachment"
|
:attachment="attachment"
|
||||||
:all-attachments="filteredCurrentChatAttachments"
|
:all-attachments="filteredCurrentChatAttachments"
|
||||||
@error="onImgError"
|
@error="onImgError"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['error'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasImgStoryError: false,
|
hasImgStoryError: false,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<DyteVideoCall
|
<DyteVideoCall
|
||||||
v-if="showDyteIntegration"
|
v-if="showDyteIntegration"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="showHead"
|
v-if="showHead"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
</button>
|
</button>
|
||||||
<GalleryView
|
<GalleryView
|
||||||
v-if="showGalleryViewer"
|
v-if="showGalleryViewer"
|
||||||
:show.sync="showGalleryViewer"
|
v-model:show="showGalleryViewer"
|
||||||
:attachment="attachment"
|
:attachment="attachment"
|
||||||
:all-attachments="availableAttachments"
|
:all-attachments="availableAttachments"
|
||||||
@error="onClose"
|
@error="onClose"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user