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