Merge branch 'develop' of https://github.com/chatwoot/chatwoot into chore/chat-list-design
This commit is contained in:
commit
52eb4c3183
1469 changed files with 35755 additions and 11179 deletions
|
@ -7,7 +7,7 @@ defaults: &defaults
|
||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: cimg/ruby:3.0.2-browsers
|
- image: cimg/ruby:3.0.4-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
|
@ -40,14 +40,13 @@ jobs:
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
- chatwoot-bundle
|
|
||||||
|
|
||||||
- run: bundle install --frozen --path ~/.bundle
|
- run: bundle install --frozen --path ~/.bundle
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- ~/.bundle
|
- ~/.bundle
|
||||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
|
|
||||||
# Only necessary if app uses webpacker or yarn in some other way
|
# Only necessary if app uses webpacker or yarn in some other way
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# pre-build stage
|
|
||||||
ARG VARIANT=3
|
ARG VARIANT=ubuntu-20.04
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
|
||||||
|
|
||||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||||
ARG USER_UID=1000
|
ARG USER_UID=1000
|
||||||
|
@ -11,23 +11,36 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# [Option] Install Node.js
|
|
||||||
ARG INSTALL_NODE="true"
|
|
||||||
ARG NODE_VERSION="lts/*"
|
|
||||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
|
||||||
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
&& apt-get -y install --no-install-recommends \
|
&& apt-get -y install --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
gnupg2 \
|
||||||
tar \
|
tar \
|
||||||
tzdata \
|
tzdata \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
|
libpq-dev \
|
||||||
yarn \
|
yarn \
|
||||||
git \
|
git \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
tmux \
|
tmux \
|
||||||
zsh
|
zsh \
|
||||||
|
git-flow \
|
||||||
|
npm
|
||||||
|
|
||||||
|
# Install rbenv and ruby
|
||||||
|
ARG RUBY_VERSION="3.0.4"
|
||||||
|
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||||
|
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||||
|
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||||
|
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
|
||||||
|
RUN git clone https://github.com/rbenv/ruby-build.git && \
|
||||||
|
PREFIX=/usr/local ./ruby-build/install.sh
|
||||||
|
|
||||||
|
RUN rbenv install $RUBY_VERSION && \
|
||||||
|
rbenv global $RUBY_VERSION && \
|
||||||
|
rbenv versions
|
||||||
|
|
||||||
# Install overmind
|
# Install overmind
|
||||||
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
||||||
|
@ -35,11 +48,25 @@ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmi
|
||||||
&& sudo mv overmind /usr/local/bin \
|
&& sudo mv overmind /usr/local/bin \
|
||||||
&& chmod +x /usr/local/bin/overmind
|
&& chmod +x /usr/local/bin/overmind
|
||||||
|
|
||||||
|
|
||||||
|
# Install gh
|
||||||
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||||
|
&& sudo apt update \
|
||||||
|
&& sudo apt install gh
|
||||||
|
|
||||||
|
|
||||||
# Do the set up required for chatwoot app
|
# Do the set up required for chatwoot app
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
COPY . /workspace
|
COPY . /workspace
|
||||||
RUN yarn
|
|
||||||
|
|
||||||
|
# set up ruby
|
||||||
COPY Gemfile Gemfile.lock ./
|
COPY Gemfile Gemfile.lock ./
|
||||||
RUN gem install bundler && bundle install
|
RUN gem install bundler && bundle install
|
||||||
|
|
||||||
|
# set up node js
|
||||||
|
RUN npm install npm@latest -g && \
|
||||||
|
npm install n -g && \
|
||||||
|
n latest
|
||||||
|
RUN npm install --global yarn
|
||||||
|
RUN yarn
|
||||||
|
|
|
@ -23,17 +23,18 @@
|
||||||
// 5432 postgres
|
// 5432 postgres
|
||||||
// 6379 redis
|
// 6379 redis
|
||||||
// 1025,8025 mailhog
|
// 1025,8025 mailhog
|
||||||
"forwardPorts": [8025],
|
"forwardPorts": [8025, 3000, 3035],
|
||||||
//your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to []
|
|
||||||
"appPort": [3000, 3035],
|
|
||||||
|
|
||||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"3000": {
|
"3000": {
|
||||||
"label": "Rails Server"
|
"label": "Rails Server"
|
||||||
},
|
},
|
||||||
|
"3035": {
|
||||||
|
"label": "Webpack Dev Server"
|
||||||
|
},
|
||||||
"8025": {
|
"8025": {
|
||||||
"label": "Mailhog UI"
|
"label": "Mailhog UI"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,8 @@ sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.de
|
||||||
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
|
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
|
||||||
# uncomment the webpacker env variable
|
# uncomment the webpacker env variable
|
||||||
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
|
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
|
||||||
|
# fix the error with webpacker
|
||||||
|
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
|
||||||
|
|
||||||
|
# codespaces make the ports public
|
||||||
|
gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME
|
||||||
|
|
|
@ -161,13 +161,15 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
## NewRelic
|
## NewRelic
|
||||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||||
# NEW_RELIC_LICENSE_KEY=
|
# NEW_RELIC_LICENSE_KEY=
|
||||||
|
# Set this to true to allow newrelic apm to send logs.
|
||||||
|
# This is turned off by default.
|
||||||
|
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
|
||||||
|
|
||||||
## Datadog
|
## Datadog
|
||||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||||
# DD_TRACE_AGENT_URL=
|
# DD_TRACE_AGENT_URL=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## IP look up configuration
|
## IP look up configuration
|
||||||
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
||||||
## works only on accounts with ip look up feature enabled
|
## works only on accounts with ip look up feature enabled
|
||||||
|
|
36
.eslintrc.js
36
.eslintrc.js
|
@ -19,18 +19,32 @@ module.exports = {
|
||||||
'jsx-a11y/label-has-for': 'off',
|
'jsx-a11y/label-has-for': 'off',
|
||||||
'jsx-a11y/anchor-is-valid': 'off',
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
'import/no-unresolved': 'off',
|
'import/no-unresolved': 'off',
|
||||||
'vue/max-attributes-per-line': ['error', {
|
'vue/max-attributes-per-line': [
|
||||||
'singleline': 20,
|
'error',
|
||||||
'multiline': {
|
{
|
||||||
'max': 1,
|
singleline: 20,
|
||||||
'allowFirstLine': false
|
multiline: {
|
||||||
|
max: 1,
|
||||||
|
allowFirstLine: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}],
|
],
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': [
|
||||||
"vue/no-v-html": 'off',
|
'error',
|
||||||
|
{
|
||||||
|
html: {
|
||||||
|
void: 'always',
|
||||||
|
normal: 'always',
|
||||||
|
component: 'always',
|
||||||
|
},
|
||||||
|
svg: 'always',
|
||||||
|
math: 'always',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-v-html': 'off',
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'import/extensions': ['off'],
|
'import/extensions': ['off'],
|
||||||
'no-console': 'error'
|
'no-console': 'error',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
@ -41,12 +55,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
|
||||||
jest: true,
|
jest: true,
|
||||||
jasmine: true
|
node: true,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
__WEBPACK_ENV__: true,
|
|
||||||
bus: true,
|
bus: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# #
|
||||||
|
# # This action will publish Chatwoot CE docker image.
|
||||||
|
# # This is set to run against merges to develop, master
|
||||||
|
# # and when tags are created.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Publish Chatwoot CE docker images
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Set Chatwoot edition
|
||||||
|
run: |
|
||||||
|
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||||
|
|
||||||
|
- name: set docker tag
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: replace docker tag if master
|
||||||
|
if: github.ref_name == 'master'
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.DOCKER_TAG }}
|
73
.github/workflows/run_foss_spec.yml
vendored
Normal file
73
.github/workflows/run_foss_spec.yml
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# #
|
||||||
|
# # This action will strip the enterprise folder
|
||||||
|
# # and run the spec.
|
||||||
|
# # This is set to run against every PR.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Run Chatwoot CE spec
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:10.8
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ""
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# needed because the postgres container does not provide a healthcheck
|
||||||
|
# tmpfs makes DB faster by using RAM
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.4 # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- name: yarn
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: yarn check-files
|
||||||
|
run: yarn install --check-files
|
||||||
|
|
||||||
|
# Run rails tests
|
||||||
|
- name: Run backend tests
|
||||||
|
run: |
|
||||||
|
bundle exec rspec --profile=10 --format documentation
|
|
@ -1 +1 @@
|
||||||
3.0.2
|
3.0.4
|
||||||
|
|
21
Gemfile
21
Gemfile
|
@ -1,6 +1,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.0.2'
|
ruby '3.0.4'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
|
@ -97,14 +97,14 @@ gem 'brakeman'
|
||||||
gem 'ddtrace'
|
gem 'ddtrace'
|
||||||
gem 'newrelic_rpm'
|
gem 'newrelic_rpm'
|
||||||
gem 'scout_apm'
|
gem 'scout_apm'
|
||||||
gem 'sentry-rails'
|
gem 'sentry-rails', '~> 5.3'
|
||||||
gem 'sentry-ruby'
|
gem 'sentry-ruby', '~> 5.3'
|
||||||
gem 'sentry-sidekiq'
|
gem 'sentry-sidekiq', '~> 5.3'
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '~> 6.4.0'
|
gem 'sidekiq', '~> 6.4.0'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron'
|
gem 'sidekiq-cron', '~> 1.3'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
|
@ -125,6 +125,12 @@ gem 'procore-sift'
|
||||||
gem 'email_reply_trimmer'
|
gem 'email_reply_trimmer'
|
||||||
gem 'html2text'
|
gem 'html2text'
|
||||||
|
|
||||||
|
# to calculate working hours
|
||||||
|
gem 'working_hours'
|
||||||
|
|
||||||
|
# full text search for articles
|
||||||
|
gem 'pg_search'
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
# we dont want request timing out in development while using byebug
|
# we dont want request timing out in development while using byebug
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
@ -153,11 +159,6 @@ group :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# TODO: is this needed ?
|
|
||||||
# errors thrown by devise password gem
|
|
||||||
gem 'flay'
|
|
||||||
gem 'rspec'
|
|
||||||
# for error thrown by devise password gem
|
|
||||||
gem 'active_record_query_trace'
|
gem 'active_record_query_trace'
|
||||||
gem 'bundle-audit', require: false
|
gem 'bundle-audit', require: false
|
||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
|
|
189
Gemfile.lock
189
Gemfile.lock
|
@ -1,6 +1,6 @@
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/chatwoot/devise-secure_password
|
remote: https://github.com/chatwoot/devise-secure_password
|
||||||
revision: de11e8765654b8242d42101ee9c8ffc8126f7975
|
revision: d777b04f12652d576b1272b8f39857e3e0b3fc26
|
||||||
specs:
|
specs:
|
||||||
devise-secure_password (2.0.1)
|
devise-secure_password (2.0.1)
|
||||||
devise (>= 4.0.0, < 5.0.0)
|
devise (>= 4.0.0, < 5.0.0)
|
||||||
|
@ -9,63 +9,63 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.4.7)
|
actioncable (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.4.7)
|
actionmailbox (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.4.7)
|
actionmailer (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.4.7)
|
actionpack (6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.4.7)
|
actiontext (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.4.7)
|
actionview (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.4.7)
|
activejob (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.4.7)
|
activemodel (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
activerecord (6.1.4.7)
|
activerecord (6.1.5.1)
|
||||||
activemodel (= 6.1.4.7)
|
activemodel (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
activerecord-import (1.3.0)
|
activerecord-import (1.3.0)
|
||||||
activerecord (>= 4.2)
|
activerecord (>= 4.2)
|
||||||
activestorage (6.1.4.7)
|
activestorage (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.4.7)
|
activesupport (6.1.5.1)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -136,7 +136,7 @@ GEM
|
||||||
climate_control (1.0.1)
|
climate_control (1.0.1)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.4)
|
commonmarker (0.23.4)
|
||||||
concurrent-ruby (1.1.9)
|
concurrent-ruby (1.1.10)
|
||||||
connection_pool (2.2.5)
|
connection_pool (2.2.5)
|
||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
|
@ -182,8 +182,7 @@ GEM
|
||||||
regexp_parser (~> 2.2)
|
regexp_parser (~> 2.2)
|
||||||
email_reply_trimmer (0.1.13)
|
email_reply_trimmer (0.1.13)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
erubis (2.7.0)
|
et-orbi (1.2.7)
|
||||||
et-orbi (1.2.6)
|
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.8.1)
|
execjs (2.8.1)
|
||||||
facebook-messenger (2.0.1)
|
facebook-messenger (2.0.1)
|
||||||
|
@ -204,14 +203,9 @@ GEM
|
||||||
faraday (~> 1)
|
faraday (~> 1)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
flag_shih_tzu (0.3.23)
|
flag_shih_tzu (0.3.23)
|
||||||
flay (2.12.1)
|
|
||||||
erubis (~> 2.7.0)
|
|
||||||
path_expander (~> 1.0)
|
|
||||||
ruby_parser (~> 3.0)
|
|
||||||
sexp_processor (~> 4.0)
|
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
fugit (1.5.2)
|
fugit (1.5.3)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
gapic-common (0.3.4)
|
gapic-common (0.3.4)
|
||||||
google-protobuf (~> 3.12, >= 3.12.2)
|
google-protobuf (~> 3.12, >= 3.12.2)
|
||||||
|
@ -309,7 +303,7 @@ GEM
|
||||||
jbuilder (2.11.5)
|
jbuilder (2.11.5)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
jmespath (1.6.0)
|
jmespath (1.6.1)
|
||||||
jquery-rails (4.4.0)
|
jquery-rails (4.4.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
|
@ -349,7 +343,7 @@ GEM
|
||||||
listen (3.7.1)
|
listen (3.7.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.14.0)
|
loofah (2.17.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -376,16 +370,16 @@ GEM
|
||||||
net-http-persistent (4.0.1)
|
net-http-persistent (4.0.1)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.4.0)
|
newrelic_rpm (8.7.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.3)
|
nokogiri (1.13.6)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-arm64-darwin)
|
nokogiri (1.13.6-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-x86_64-darwin)
|
nokogiri (1.13.6-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-x86_64-linux)
|
nokogiri (1.13.6-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.8)
|
oauth (0.5.8)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -393,8 +387,10 @@ GEM
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.1.1.0)
|
parser (3.1.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
path_expander (1.1.0)
|
|
||||||
pg (1.3.2)
|
pg (1.3.2)
|
||||||
|
pg_search (2.3.6)
|
||||||
|
activerecord (>= 5.2)
|
||||||
|
activesupport (>= 5.2)
|
||||||
procore-sift (0.16.0)
|
procore-sift (0.16.0)
|
||||||
rails (> 4.2.0)
|
rails (> 4.2.0)
|
||||||
pry (0.14.1)
|
pry (0.14.1)
|
||||||
|
@ -403,13 +399,13 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.6.2)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3.1)
|
||||||
rack-attack (6.6.0)
|
rack-attack (6.6.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
|
@ -419,31 +415,31 @@ GEM
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-timeout (0.6.0)
|
rack-timeout (0.6.0)
|
||||||
rails (6.1.4.7)
|
rails (6.1.5.1)
|
||||||
actioncable (= 6.1.4.7)
|
actioncable (= 6.1.5.1)
|
||||||
actionmailbox (= 6.1.4.7)
|
actionmailbox (= 6.1.5.1)
|
||||||
actionmailer (= 6.1.4.7)
|
actionmailer (= 6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
actiontext (= 6.1.4.7)
|
actiontext (= 6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activemodel (= 6.1.4.7)
|
activemodel (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.4.7)
|
railties (= 6.1.5.1)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.2)
|
rails-html-sanitizer (1.4.2)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.1.4.7)
|
railties (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
|
@ -468,10 +464,6 @@ GEM
|
||||||
netrc (~> 0.8)
|
netrc (~> 0.8)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rspec (3.11.0)
|
|
||||||
rspec-core (~> 3.11.0)
|
|
||||||
rspec-expectations (~> 3.11.0)
|
|
||||||
rspec-mocks (~> 3.11.0)
|
|
||||||
rspec-core (3.11.0)
|
rspec-core (3.11.0)
|
||||||
rspec-support (~> 3.11.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-expectations (3.11.0)
|
rspec-expectations (3.11.0)
|
||||||
|
@ -533,16 +525,16 @@ GEM
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sentry-rails (5.1.0)
|
sentry-rails (5.3.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby-core (~> 5.1.0)
|
sentry-ruby-core (~> 5.3.0)
|
||||||
sentry-ruby (5.1.0)
|
sentry-ruby (5.3.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-ruby-core (= 5.1.0)
|
sentry-ruby-core (= 5.3.0)
|
||||||
sentry-ruby-core (5.1.0)
|
sentry-ruby-core (5.3.0)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
sentry-sidekiq (5.1.0)
|
sentry-sidekiq (5.3.0)
|
||||||
sentry-ruby-core (~> 5.1.0)
|
sentry-ruby-core (~> 5.3.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.16.0)
|
sexp_processor (4.16.0)
|
||||||
shoulda-matchers (5.1.0)
|
shoulda-matchers (5.1.0)
|
||||||
|
@ -551,8 +543,8 @@ GEM
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
sidekiq-cron (1.2.0)
|
sidekiq-cron (1.4.0)
|
||||||
fugit (~> 1.1)
|
fugit (~> 1)
|
||||||
sidekiq (>= 4.2.1)
|
sidekiq (>= 4.2.1)
|
||||||
signet (0.16.0)
|
signet (0.16.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
|
@ -636,6 +628,9 @@ GEM
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
|
working_hours (1.4.1)
|
||||||
|
activesupport (>= 3.2)
|
||||||
|
tzinfo
|
||||||
zeitwerk (2.5.4)
|
zeitwerk (2.5.4)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
@ -678,7 +673,6 @@ DEPENDENCIES
|
||||||
faker
|
faker
|
||||||
fcm
|
fcm
|
||||||
flag_shih_tzu
|
flag_shih_tzu
|
||||||
flay
|
|
||||||
foreman
|
foreman
|
||||||
geocoder
|
geocoder
|
||||||
google-cloud-dialogflow
|
google-cloud-dialogflow
|
||||||
|
@ -703,6 +697,7 @@ DEPENDENCIES
|
||||||
mock_redis
|
mock_redis
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
pg
|
pg
|
||||||
|
pg_search
|
||||||
procore-sift
|
procore-sift
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
@ -715,7 +710,6 @@ DEPENDENCIES
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec
|
|
||||||
rspec-rails (~> 5.0.0)
|
rspec-rails (~> 5.0.0)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
@ -723,12 +717,12 @@ DEPENDENCIES
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
scout_apm
|
scout_apm
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails
|
sentry-rails (~> 5.3)
|
||||||
sentry-ruby
|
sentry-ruby (~> 5.3)
|
||||||
sentry-sidekiq
|
sentry-sidekiq (~> 5.3)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (~> 6.4.0)
|
sidekiq (~> 6.4.0)
|
||||||
sidekiq-cron
|
sidekiq-cron (~> 1.3)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client
|
slack-ruby-client
|
||||||
spring
|
spring
|
||||||
|
@ -746,9 +740,10 @@ DEPENDENCIES
|
||||||
webpacker (~> 5.x)
|
webpacker (~> 5.x)
|
||||||
webpush
|
webpush
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
|
working_hours
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.0.2p107
|
ruby 3.0.4p208
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.8
|
2.3.15
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Campaigns::CampaignConversationBuilder
|
class Campaigns::CampaignConversationBuilder
|
||||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes]
|
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||||
|
@ -21,7 +21,8 @@ class Campaigns::CampaignConversationBuilder
|
||||||
|
|
||||||
def message_params
|
def message_params
|
||||||
ActionController::Parameters.new({
|
ActionController::Parameters.new({
|
||||||
content: @campaign.message
|
content: @campaign.message,
|
||||||
|
campaign_id: @campaign.id
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -32,7 +33,8 @@ class Campaigns::CampaignConversationBuilder
|
||||||
contact_id: @contact_inbox.contact_id,
|
contact_id: @contact_inbox.contact_id,
|
||||||
contact_inbox_id: @contact_inbox.id,
|
contact_inbox_id: @contact_inbox.id,
|
||||||
campaign_id: @campaign.id,
|
campaign_id: @campaign.id,
|
||||||
additional_attributes: conversation_additional_attributes
|
additional_attributes: conversation_additional_attributes,
|
||||||
|
custom_attributes: custom_attributes || {}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,11 +15,10 @@ class ContactBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(contact)
|
def create_contact_inbox(contact)
|
||||||
::ContactInbox.create!(
|
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
inbox_id: inbox.id,
|
inbox_id: inbox.id,
|
||||||
source_id: source_id,
|
source_id: source_id
|
||||||
hmac_verified: hmac_verified || false
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
ensure_contact_avatar
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
return if contact.present?
|
return if contact.present?
|
||||||
|
|
||||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
|
@ -128,10 +128,10 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
result = {}
|
result = {}
|
||||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||||
Sentry.capture_exception(e) unless @outgoing_echo
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result = {}
|
result = {}
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
end
|
end
|
||||||
process_contact_params_result(result)
|
process_contact_params_result(result)
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Messages::MessageBuilder
|
||||||
@user = user
|
@user = user
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@message_type = params[:message_type] || 'outgoing'
|
||||||
@attachments = params[:attachments]
|
@attachments = params[:attachments]
|
||||||
|
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||||
return unless params.instance_of?(ActionController::Parameters)
|
return unless params.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||||
|
@ -64,6 +65,18 @@ class Messages::MessageBuilder
|
||||||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def automation_rule_id
|
||||||
|
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def campaign_id
|
||||||
|
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
@params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {}
|
||||||
|
end
|
||||||
|
|
||||||
def message_sender
|
def message_sender
|
||||||
return if @params[:sender_type] != 'AgentBot'
|
return if @params[:sender_type] != 'AgentBot'
|
||||||
|
|
||||||
|
@ -82,6 +95,6 @@ class Messages::MessageBuilder
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id]
|
||||||
}.merge(external_created_at)
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,16 +53,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
|
|
||||||
def fetch_story_link(attachment)
|
def fetch_story_link(attachment)
|
||||||
message = attachment.message
|
message = attachment.message
|
||||||
begin
|
result = get_story_object_from_source_id(message.source_id)
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
|
||||||
result = k.get_object(message.source_id, fields: %w[story from]) || {}
|
|
||||||
rescue Koala::Facebook::AuthenticationError
|
|
||||||
@inbox.channel.authorization_error!
|
|
||||||
raise
|
|
||||||
rescue StandardError => e
|
|
||||||
result = {}
|
|
||||||
Sentry.capture_exception(e)
|
|
||||||
end
|
|
||||||
story_id = result['story']['mention']['id']
|
story_id = result['story']['mention']['id']
|
||||||
story_sender = result['from']['username']
|
story_sender = result['from']['username']
|
||||||
message.content_attributes[:story_sender] = story_sender
|
message.content_attributes[:story_sender] = story_sender
|
||||||
|
@ -70,4 +61,15 @@ class Messages::Messenger::MessageBuilder
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||||
message.save!
|
message.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_story_object_from_source_id(source_id)
|
||||||
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
|
k.get_object(source_id, fields: %w[story from]) || {}
|
||||||
|
rescue Koala::Facebook::AuthenticationError
|
||||||
|
@inbox.channel.authorization_error!
|
||||||
|
raise
|
||||||
|
rescue StandardError => e
|
||||||
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ class V2::ReportBuilder
|
||||||
attr_reader :account, :params
|
attr_reader :account, :params
|
||||||
|
|
||||||
DEFAULT_GROUP_BY = 'day'.freeze
|
DEFAULT_GROUP_BY = 'day'.freeze
|
||||||
|
AGENT_RESULTS_PER_PAGE = 25
|
||||||
|
|
||||||
def initialize(account, params)
|
def initialize(account, params)
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -45,7 +46,7 @@ class V2::ReportBuilder
|
||||||
if params[:type].equal?(:account)
|
if params[:type].equal?(:account)
|
||||||
conversations
|
conversations
|
||||||
else
|
else
|
||||||
agent_metrics
|
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -79,20 +80,23 @@ class V2::ReportBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent_metrics
|
def agent_metrics
|
||||||
users = @account.users
|
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
|
||||||
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
account_users.each_with_object([]) do |account_user, arr|
|
||||||
users.each_with_object([]) do |user, arr|
|
@user = account_user.user
|
||||||
@user = user
|
|
||||||
arr << {
|
arr << {
|
||||||
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
id: @user.id,
|
||||||
|
name: @user.name,
|
||||||
|
email: @user.email,
|
||||||
|
thumbnail: @user.avatar_url,
|
||||||
|
availability: account_user.availability_status,
|
||||||
metric: conversations
|
metric: conversations
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
@open_conversations = scope.conversations.open
|
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||||
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||||
metric = {
|
metric = {
|
||||||
open: @open_conversations.count,
|
open: @open_conversations.count,
|
||||||
unattended: @open_conversations.count - first_response_count
|
unattended: @open_conversations.count - first_response_count
|
||||||
|
|
|
@ -45,6 +45,8 @@ class RoomChannel < ApplicationCable::Channel
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
return if current_user.blank?
|
||||||
|
|
||||||
@current_account ||= if @current_user.is_a? Contact
|
@current_account ||= if @current_user.is_a? Contact
|
||||||
@current_user.account
|
@current_user.account
|
||||||
else
|
else
|
||||||
|
|
48
app/controllers/api/v1/accounts/articles_controller.rb
Normal file
48
app/controllers/api/v1/accounts/articles_controller.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :portal
|
||||||
|
before_action :fetch_article, except: [:index, :create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@articles = @portal.articles
|
||||||
|
@articles.search(list_params) if params[:payload].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@article = @portal.articles.create!(article_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@article.update!(article_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@article.destroy!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_article
|
||||||
|
@article = @portal.articles.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def portal
|
||||||
|
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def article_params
|
||||||
|
params.require(:article).permit(
|
||||||
|
:title, :content, :description, :position, :category_id, :author_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_params
|
||||||
|
params.require(:payload).permit(
|
||||||
|
:category_slug, :locale, :query
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Api::V1::Accounts::AssignableAgentsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :fetch_inboxes
|
||||||
|
|
||||||
|
def index
|
||||||
|
agent_ids = @inboxes.map do |inbox|
|
||||||
|
authorize inbox, :show?
|
||||||
|
member_ids = inbox.members.pluck(:user_id)
|
||||||
|
member_ids
|
||||||
|
end
|
||||||
|
agent_ids = agent_ids.inject(:&)
|
||||||
|
agents = Current.account.users.where(id: agent_ids)
|
||||||
|
@assignable_agents = (agents + Current.account.administrators).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_inboxes
|
||||||
|
@inboxes = Current.account.inboxes.find(permitted_params[:inbox_ids])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(inbox_ids: [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
def create
|
def create
|
||||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||||
@automation_rule.actions = params[:actions]
|
@automation_rule.actions = params[:actions]
|
||||||
|
@automation_rule.conditions = params[:conditions]
|
||||||
|
|
||||||
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||||
|
|
||||||
|
@ -17,12 +18,27 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
@automation_rule
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_file
|
||||||
|
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
key: nil,
|
||||||
|
io: params[:attachment].tempfile,
|
||||||
|
filename: params[:attachment].original_filename,
|
||||||
|
content_type: params[:attachment].content_type
|
||||||
|
)
|
||||||
|
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@automation_rule.update(automation_rules_permit)
|
ActiveRecord::Base.transaction do
|
||||||
process_attachments
|
automation_rule_update
|
||||||
@automation_rule
|
process_attachments
|
||||||
|
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error e
|
||||||
|
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -37,20 +53,30 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
@automation_rule = new_rule
|
@automation_rule = new_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_attachments
|
||||||
|
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||||
|
return if actions.blank?
|
||||||
|
|
||||||
|
actions.each do |action|
|
||||||
|
blob_id = action['action_params']
|
||||||
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||||
|
@automation_rule.files.attach(blob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_attachments
|
def automation_rule_update
|
||||||
return if params[:attachments].blank?
|
@automation_rule.update!(automation_rules_permit)
|
||||||
|
@automation_rule.actions = params[:actions] if params[:actions]
|
||||||
params[:attachments].each do |uploaded_attachment|
|
@automation_rule.conditions = params[:conditions] if params[:conditions]
|
||||||
@automation_rule.files.attach(uploaded_attachment)
|
@automation_rule.save!
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def automation_rules_permit
|
def automation_rules_permit
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :description, :event_name, :account_id, :active,
|
:name, :description, :event_name, :account_id, :active,
|
||||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
conditions: [:attribute_key, :filter_operator, :query_operator, :custom_attribute_type, { values: [] }],
|
||||||
actions: [:action_name, { action_params: [] }]
|
actions: [:action_name, { action_params: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
set_instagram_id(page_access_token, facebook_channel)
|
set_instagram_id(page_access_token, facebook_channel)
|
||||||
set_avatar(@facebook_inbox, page_id)
|
set_avatar(@facebook_inbox, page_id)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
set_instagram_id(access_token, fb_page)
|
set_instagram_id(access_token, fb_page)
|
||||||
fb_page&.reauthorized!
|
fb_page&.reauthorized!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController
|
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :portal
|
||||||
before_action :fetch_category, except: [:index, :create]
|
before_action :fetch_category, except: [:index, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories.search(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -24,9 +25,13 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
|
||||||
@category = @portal.categories.find(params[:id])
|
@category = @portal.categories.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def portal
|
||||||
|
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||||
|
end
|
||||||
|
|
||||||
def category_params
|
def category_params
|
||||||
params.require(:category).permit(
|
params.require(:category).permit(
|
||||||
:name, :description, :position
|
:name, :description, :position, :slug, :locale
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -7,7 +7,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
build_inbox
|
build_inbox
|
||||||
setup_webhooks if @twilio_channel.sms?
|
setup_webhooks if @twilio_channel.sms?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
|
||||||
authorize @conversation.inbox, :show?
|
authorize @conversation.inbox, :show?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||||
RESULTS_PER_PAGE = 25
|
RESULTS_PER_PAGE = 25
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_csat_survey_responses, only: [:index, :metrics]
|
before_action :set_csat_survey_responses, only: [:index, :metrics, :download]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
before_action :set_current_page_surveys, only: [:index]
|
before_action :set_current_page_surveys, only: [:index]
|
||||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||||
|
@ -19,6 +19,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||||
@ratings_count = @csat_survey_responses.group(:rating).count
|
@ratings_count = @csat_survey_responses.group(:rating).count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def download
|
||||||
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
|
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
|
||||||
|
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_total_sent_messages_count
|
def set_total_sent_messages_count
|
||||||
|
|
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :fetch_dashboard_apps, except: [:create]
|
||||||
|
before_action :fetch_dashboard_app, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@dashboard_app = Current.account.dashboard_apps.create!(
|
||||||
|
permitted_payload.merge(user_id: Current.user.id)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@dashboard_app.update!(permitted_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@dashboard_app.destroy!
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_dashboard_apps
|
||||||
|
@dashboard_apps = Current.account.dashboard_apps
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_dashboard_app
|
||||||
|
@dashboard_app = @dashboard_apps.find(permitted_params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_payload
|
||||||
|
params.require(:dashboard_app).permit(
|
||||||
|
:title,
|
||||||
|
content: [:url, :type]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
|
# Deprecated: This API will be removed in 2.7.0
|
||||||
def assignable_agents
|
def assignable_agents
|
||||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||||
end
|
end
|
||||||
|
@ -41,15 +42,19 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@inbox.update(permitted_params.except(:channel))
|
@inbox.update!(permitted_params.except(:channel))
|
||||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
update_inbox_working_hours
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||||
|
|
||||||
# Inbox update doesn't necessarily need channel attributes
|
# Inbox update doesn't necessarily need channel attributes
|
||||||
return if permitted_params(channel_attributes)[:channel].blank?
|
return if permitted_params(channel_attributes)[:channel].blank?
|
||||||
|
|
||||||
if @inbox.inbox_type == 'Email'
|
if @inbox.inbox_type == 'Email'
|
||||||
validate_email_channel(channel_attributes)
|
begin
|
||||||
|
validate_email_channel(channel_attributes)
|
||||||
|
rescue StandardError => e
|
||||||
|
render json: { message: e }, status: :unprocessable_entity and return
|
||||||
|
end
|
||||||
@inbox.channel.reauthorized!
|
@inbox.channel.reauthorized!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,6 +62,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
update_channel_feature_flags
|
update_channel_feature_flags
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_inbox_working_hours
|
||||||
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
|
end
|
||||||
|
|
||||||
def agent_bot
|
def agent_bot
|
||||||
@agent_bot = @inbox.agent_bot
|
@agent_bot = @inbox.agent_bot
|
||||||
end
|
end
|
||||||
|
@ -88,12 +97,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox_name(channel)
|
|
||||||
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
|
||||||
|
|
||||||
permitted_params[:name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_channel
|
def create_channel
|
||||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||||
|
|
||||||
|
@ -108,10 +111,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
@inbox.channel.save!
|
@inbox.channel.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox_attributes
|
||||||
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
*inbox_attributes,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
|
||||||
channel: [:type, *channel_attributes]
|
channel: [:type, *channel_attributes]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -128,18 +135,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
}[permitted_params[:channel][:type]]
|
}[permitted_params[:channel][:type]]
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_channels_method
|
|
||||||
{
|
|
||||||
'web_widget' => Current.account.web_widgets,
|
|
||||||
'api' => Current.account.api_channels,
|
|
||||||
'email' => Current.account.email_channels,
|
|
||||||
'line' => Current.account.line_channels,
|
|
||||||
'telegram' => Current.account.telegram_channels,
|
|
||||||
'whatsapp' => Current.account.whatsapp_channels,
|
|
||||||
'sms' => Current.account.sms_channels
|
|
||||||
}[permitted_params[:channel][:type]]
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_channel_attributes(channel_type)
|
def get_channel_attributes(channel_type)
|
||||||
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
||||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||||
|
@ -147,10 +142,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_limit
|
|
||||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
|
||||||
|
|
||||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :portal
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def portal
|
|
||||||
@portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,12 +1,14 @@
|
||||||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController
|
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_portal, except: [:index, :create]
|
before_action :fetch_portal, except: [:index, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@portals = Current.account.kbase_portals
|
@portals = Current.account.portals
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@portal = Current.account.kbase_portals.create!(portal_params)
|
@portal = Current.account.portals.create!(portal_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -21,12 +23,16 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_portal
|
def fetch_portal
|
||||||
@portal = current_account.kbase_portals.find(params[:id])
|
@portal = Current.account.portals.find_by(slug: permitted_params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal_params
|
def portal_params
|
||||||
params.require(:portal).permit(
|
params.require(:portal).permit(
|
||||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug
|
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def webhook_params
|
def webhook_params
|
||||||
params.require(:webhook).permit(:inbox_id, :url)
|
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_webhook
|
def fetch_webhook
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||||
twitter_consumer.consume
|
twitter_consumer.consume
|
||||||
head :ok
|
head :ok
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
browser: browser_params,
|
browser: browser_params,
|
||||||
referer: permitted_params[:message][:referer_url],
|
referer: permitted_params[:message][:referer_url],
|
||||||
initiated_at: timestamp_params
|
initiated_at: timestamp_params
|
||||||
}
|
},
|
||||||
|
custom_attributes: permitted_params[:custom_attributes].presence || {}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,16 +53,39 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
mergee_contact: @contact
|
mergee_contact: @contact
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@contact.update!(email: email, name: contact_name)
|
@contact.update!(email: email)
|
||||||
|
update_contact_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_contact_phone_number(phone_number)
|
||||||
|
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
|
||||||
|
if contact_with_phone_number
|
||||||
|
@contact = ::ContactMergeAction.new(
|
||||||
|
account: @current_account,
|
||||||
|
base_contact: contact_with_phone_number,
|
||||||
|
mergee_contact: @contact
|
||||||
|
).perform
|
||||||
|
else
|
||||||
|
@contact.update!(phone_number: phone_number)
|
||||||
|
update_contact_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact_name
|
||||||
|
@contact.update!(name: contact_name) if contact_name.present?
|
||||||
|
end
|
||||||
|
|
||||||
def contact_email
|
def contact_email
|
||||||
permitted_params[:contact][:email].downcase
|
permitted_params.dig(:contact, :email)&.downcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_name
|
def contact_name
|
||||||
params[:contact][:name] || contact_email.split('@')[0]
|
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_phone_number
|
||||||
|
permitted_params.dig(:contact, :phone_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
def browser_params
|
def browser_params
|
||||||
|
|
|
@ -7,12 +7,17 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
process_update_contact
|
||||||
@conversation = create_conversation
|
@conversation = create_conversation
|
||||||
conversation.messages.create(message_params)
|
conversation.messages.create(message_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_update_contact
|
||||||
|
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||||
|
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
||||||
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
head :ok && return if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
@ -45,7 +50,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
head :not_found && return if conversation.nil?
|
return head :not_found if conversation.nil?
|
||||||
|
|
||||||
|
return head :forbidden unless @web_widget.end_conversation?
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
conversation.status = :resolved
|
conversation.status = :resolved
|
||||||
conversation.save
|
conversation.save
|
||||||
|
@ -60,6 +68,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||||
|
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||||
|
custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
|
include Api::V2::Accounts::ReportsHelper
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -12,27 +13,23 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_agents_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=agents_report.csv'
|
generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/agents.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_inboxes_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=inboxes_report.csv'
|
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_labels_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
|
generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_teams_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
|
@ -43,46 +40,53 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def generate_csv(filename, template)
|
||||||
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
|
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
|
||||||
|
render layout: false, template: template, format: 'csv'
|
||||||
|
end
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_summary_params
|
def common_params
|
||||||
{
|
{
|
||||||
type: params[:type].to_sym,
|
type: params[:type].to_sym,
|
||||||
id: params[:id],
|
id: params[:id],
|
||||||
since: range[:current][:since],
|
group_by: params[:group_by],
|
||||||
until: range[:current][:until],
|
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||||
group_by: params[:group_by]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_summary_params
|
||||||
|
common_params.merge({
|
||||||
|
since: range[:current][:since],
|
||||||
|
until: range[:current][:until]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def previous_summary_params
|
def previous_summary_params
|
||||||
{
|
common_params.merge({
|
||||||
type: params[:type].to_sym,
|
since: range[:previous][:since],
|
||||||
id: params[:id],
|
until: range[:previous][:until]
|
||||||
since: range[:previous][:since],
|
})
|
||||||
until: range[:previous][:until],
|
|
||||||
group_by: params[:group_by]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_params
|
def report_params
|
||||||
{
|
common_params.merge({
|
||||||
metric: params[:metric],
|
metric: params[:metric],
|
||||||
type: params[:type].to_sym,
|
since: params[:since],
|
||||||
since: params[:since],
|
until: params[:until],
|
||||||
until: params[:until],
|
timezone_offset: params[:timezone_offset]
|
||||||
id: params[:id],
|
})
|
||||||
group_by: params[:group_by],
|
|
||||||
timezone_offset: params[:timezone_offset]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_params
|
def conversation_params
|
||||||
{
|
{
|
||||||
type: params[:type].to_sym,
|
type: params[:type].to_sym,
|
||||||
user_id: params[:user_id]
|
user_id: params[:user_id],
|
||||||
|
page: params[:page].presence || 1
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||||
include RequestExceptionHandler
|
include RequestExceptionHandler
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
|
@ -9,8 +9,7 @@ module RequestExceptionHandler
|
||||||
|
|
||||||
def handle_with_exception
|
def handle_with_exception
|
||||||
yield
|
yield
|
||||||
rescue ActiveRecord::RecordNotFound => e
|
rescue ActiveRecord::RecordNotFound
|
||||||
Sentry.capture_exception(e)
|
|
||||||
render_not_found_error('Resource could not be found')
|
render_not_found_error('Resource could not be found')
|
||||||
rescue Pundit::NotAuthorizedError
|
rescue Pundit::NotAuthorizedError
|
||||||
render_unauthorized('You are not authorized to do this action')
|
render_unauthorized('You are not authorized to do this action')
|
||||||
|
|
|
@ -38,9 +38,13 @@ class DashboardController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def app_config
|
def app_config
|
||||||
{ APP_VERSION: Chatwoot.config[:version],
|
{
|
||||||
|
APP_VERSION: Chatwoot.config[:version],
|
||||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') }
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||||
|
FACEBOOK_API_VERSION: 'v14.0',
|
||||||
|
IS_ENTERPRISE: ChatwootApp.enterprise?
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,10 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.assign_attributes(user_update_params)
|
@resource.assign_attributes(user_update_params)
|
||||||
|
|
||||||
|
# We are using devise's reconfirmable flow for changing emails
|
||||||
|
# But in case of platform APIs we don't want user to go through this extra step
|
||||||
|
@resource.skip_reconfirmation! if user_update_params[:email].present?
|
||||||
@resource.save!
|
@resource.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base
|
||||||
before_action :ensure_widget_position
|
before_action :ensure_widget_position
|
||||||
before_action :ensure_widget_type
|
before_action :ensure_widget_type
|
||||||
before_action :ensure_widget_style
|
before_action :ensure_widget_style
|
||||||
|
before_action :ensure_dark_mode
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render
|
render
|
||||||
|
@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base
|
||||||
@widget_style = params[:widget_style] || 'standard'
|
@widget_style = params[:widget_style] || 'standard'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_dark_mode
|
||||||
|
@dark_mode = params[:dark_mode] || 'light'
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_widget_position
|
def ensure_widget_position
|
||||||
@widget_position = params[:position] || 'left'
|
@widget_position = params[:position] || 'left'
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,6 +51,7 @@ class ConversationFinder
|
||||||
filter_by_team if @team
|
filter_by_team if @team
|
||||||
filter_by_labels if params[:labels]
|
filter_by_labels if params[:labels]
|
||||||
filter_by_query if params[:q]
|
filter_by_query if params[:q]
|
||||||
|
filter_by_reply_status
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_inboxes
|
def set_inboxes
|
||||||
|
@ -90,6 +91,10 @@ class ConversationFinder
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by_reply_status
|
||||||
|
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
|
||||||
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
||||||
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
||||||
|
|
15
app/finders/email_channel_finder.rb
Normal file
15
app/finders/email_channel_finder.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class EmailChannelFinder
|
||||||
|
def initialize(email_object)
|
||||||
|
@email_object = email_object
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
channel = nil
|
||||||
|
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
|
||||||
|
recipient_mails.each do |email|
|
||||||
|
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
||||||
|
break if channel.present?
|
||||||
|
end
|
||||||
|
channel
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,7 +22,7 @@ class MessageFinder
|
||||||
|
|
||||||
def current_messages
|
def current_messages
|
||||||
if @params[:before].present?
|
if @params[:before].present?
|
||||||
messages.reorder('created_at desc').where('id < ?', @params[:before]).limit(20).reverse
|
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
||||||
else
|
else
|
||||||
messages.reorder('created_at desc').limit(20).reverse
|
messages.reorder('created_at desc').limit(20).reverse
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
module Api::V1::InboxesHelper
|
module Api::V1::InboxesHelper
|
||||||
|
def inbox_name(channel)
|
||||||
|
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
||||||
|
|
||||||
|
permitted_params[:name]
|
||||||
|
end
|
||||||
|
|
||||||
def validate_email_channel(attributes)
|
def validate_email_channel(attributes)
|
||||||
channel_data = permitted_params(attributes)[:channel]
|
channel_data = permitted_params(attributes)[:channel]
|
||||||
|
|
||||||
|
@ -14,13 +20,12 @@ module Api::V1::InboxesHelper
|
||||||
Mail.defaults do
|
Mail.defaults do
|
||||||
retriever_method :imap, { address: channel_data[:imap_address],
|
retriever_method :imap, { address: channel_data[:imap_address],
|
||||||
port: channel_data[:imap_port],
|
port: channel_data[:imap_port],
|
||||||
user_name: channel_data[:imap_email],
|
user_name: channel_data[:imap_login],
|
||||||
password: channel_data[:imap_password],
|
password: channel_data[:imap_password],
|
||||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||||
end
|
end
|
||||||
|
|
||||||
Mail.connection do # rubocop:disable:block
|
check_imap_connection(channel_data)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_smtp(channel_data)
|
def validate_smtp(channel_data)
|
||||||
|
@ -29,8 +34,31 @@ module Api::V1::InboxesHelper
|
||||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||||
|
|
||||||
set_smtp_encryption(channel_data, smtp)
|
set_smtp_encryption(channel_data, smtp)
|
||||||
|
check_smtp_connection(channel_data, smtp)
|
||||||
|
end
|
||||||
|
|
||||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login)
|
def check_imap_connection(channel_data)
|
||||||
|
Mail.connection {} # rubocop:disable:block
|
||||||
|
rescue SocketError => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
|
||||||
|
rescue Net::IMAP::NoResponseError => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.no_response_error')
|
||||||
|
rescue Errno::EHOSTUNREACH => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error')
|
||||||
|
rescue Net::OpenTimeout => e
|
||||||
|
raise StandardError,
|
||||||
|
I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port])
|
||||||
|
rescue Net::IMAP::Error => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error')
|
||||||
|
rescue StandardError => e
|
||||||
|
raise StandardError, e.message
|
||||||
|
ensure
|
||||||
|
Rails.logger.error e if e.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_smtp_connection(channel_data, smtp)
|
||||||
|
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||||
|
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||||
smtp.finish unless smtp&.nil?
|
smtp.finish unless smtp&.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,4 +98,22 @@ module Api::V1::InboxesHelper
|
||||||
context.verify_mode = openssl_verify_mode
|
context.verify_mode = openssl_verify_mode
|
||||||
context
|
context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_channels_method
|
||||||
|
{
|
||||||
|
'web_widget' => Current.account.web_widgets,
|
||||||
|
'api' => Current.account.api_channels,
|
||||||
|
'email' => Current.account.email_channels,
|
||||||
|
'line' => Current.account.line_channels,
|
||||||
|
'telegram' => Current.account.telegram_channels,
|
||||||
|
'whatsapp' => Current.account.whatsapp_channels,
|
||||||
|
'sms' => Current.account.sms_channels
|
||||||
|
}[permitted_params[:channel][:type]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_limit
|
||||||
|
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||||
|
|
||||||
|
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
56
app/helpers/api/v2/accounts/reports_helper.rb
Normal file
56
app/helpers/api/v2/accounts/reports_helper.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
module Api::V2::Accounts::ReportsHelper
|
||||||
|
def generate_agents_report
|
||||||
|
Current.account.users.map do |agent|
|
||||||
|
agent_report = generate_report({ type: :agent, id: agent.id })
|
||||||
|
[agent.name] + generate_readable_report_metrics(agent_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_inboxes_report
|
||||||
|
Current.account.inboxes.map do |inbox|
|
||||||
|
inbox_report = generate_report({ type: :inbox, id: inbox.id })
|
||||||
|
[inbox.name, inbox.channel&.name] + generate_readable_report_metrics(inbox_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_teams_report
|
||||||
|
Current.account.teams.map do |team|
|
||||||
|
team_report = generate_report({ type: :team, id: team.id })
|
||||||
|
[team.name] + generate_readable_report_metrics(team_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_labels_report
|
||||||
|
Current.account.labels.map do |label|
|
||||||
|
label_report = generate_report({ type: :label, id: label.id })
|
||||||
|
[label.title] + generate_readable_report_metrics(label_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_report(report_params)
|
||||||
|
V2::ReportBuilder.new(
|
||||||
|
Current.account,
|
||||||
|
report_params.merge(
|
||||||
|
{
|
||||||
|
since: params[:since],
|
||||||
|
until: params[:until],
|
||||||
|
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).summary
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_readable_report_metrics(report_metric)
|
||||||
|
[
|
||||||
|
report_metric[:conversations_count],
|
||||||
|
time_to_minutes(report_metric[:avg_first_response_time]),
|
||||||
|
time_to_minutes(report_metric[:avg_resolution_time])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def time_to_minutes(time_in_seconds)
|
||||||
|
(time_in_seconds / 60).to_i
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,33 +17,39 @@ module ReportHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations_count
|
def conversations_count
|
||||||
(get_grouped_values scope.conversations).count
|
(get_grouped_values scope.conversations.where(account_id: account.id)).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_messages_count
|
def incoming_messages_count
|
||||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
(get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_messages_count
|
def outgoing_messages_count
|
||||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
(get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolutions_count
|
def resolutions_count
|
||||||
(get_grouped_values scope.conversations.resolved).count
|
(get_grouped_values scope.conversations.where(account_id: account.id).resolved).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_first_response_time
|
def avg_first_response_time
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id))
|
||||||
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
|
|
||||||
|
grouped_reporting_events.average(:value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_resolution_time
|
def avg_resolution_time
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id))
|
||||||
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
|
|
||||||
|
grouped_reporting_events.average(:value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_resolution_time_summary
|
def avg_resolution_time_summary
|
||||||
avg_rt = scope.reporting_events
|
reporting_events = scope.reporting_events
|
||||||
.where(name: 'conversation_resolved', created_at: range)
|
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
|
||||||
.average(:value)
|
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||||
|
|
||||||
return 0 if avg_rt.blank?
|
return 0 if avg_rt.blank?
|
||||||
|
|
||||||
|
@ -51,9 +57,9 @@ module ReportHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_first_response_time_summary
|
def avg_first_response_time_summary
|
||||||
avg_frt = scope.reporting_events
|
reporting_events = scope.reporting_events
|
||||||
.where(name: 'first_response', created_at: range)
|
.where(name: 'first_response', account_id: account.id, created_at: range)
|
||||||
.average(:value)
|
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||||
|
|
||||||
return 0 if avg_frt.blank?
|
return 0 if avg_frt.blank?
|
||||||
|
|
||||||
|
|
50
app/helpers/reporting_event_helper.rb
Normal file
50
app/helpers/reporting_event_helper.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
module ReportingEventHelper
|
||||||
|
def business_hours(inbox, from, to)
|
||||||
|
return 0 unless inbox.working_hours_enabled?
|
||||||
|
|
||||||
|
inbox_working_hours = configure_working_hours(inbox.working_hours)
|
||||||
|
return 0 if inbox_working_hours.blank?
|
||||||
|
|
||||||
|
# Configure working hours
|
||||||
|
WorkingHours::Config.working_hours = inbox_working_hours
|
||||||
|
|
||||||
|
# Configure timezone
|
||||||
|
WorkingHours::Config.time_zone = inbox.timezone
|
||||||
|
|
||||||
|
# Use inbox timezone to change from & to values.
|
||||||
|
from_in_inbox_timezone = from.in_time_zone(inbox.timezone).to_time
|
||||||
|
to_in_inbox_timezone = to.in_time_zone(inbox.timezone).to_time
|
||||||
|
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def configure_working_hours(working_hours)
|
||||||
|
working_hours.each_with_object({}) do |working_hour, object|
|
||||||
|
object[day(working_hour.day_of_week)] = working_hour_range(working_hour) unless working_hour.closed_all_day?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def day(day_of_week)
|
||||||
|
week_days = {
|
||||||
|
0 => :sun,
|
||||||
|
1 => :mon,
|
||||||
|
2 => :tue,
|
||||||
|
3 => :wed,
|
||||||
|
4 => :thu,
|
||||||
|
5 => :fri,
|
||||||
|
6 => :sat
|
||||||
|
}
|
||||||
|
week_days[day_of_week]
|
||||||
|
end
|
||||||
|
|
||||||
|
def working_hour_range(working_hour)
|
||||||
|
{ format_time(working_hour.open_hour, working_hour.open_minutes) => format_time(working_hour.close_hour, working_hour.close_minutes) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_time(hour, minute)
|
||||||
|
hour = hour < 10 ? "0#{hour}" : hour
|
||||||
|
minute = minute < 10 ? "0#{minute}" : minute
|
||||||
|
"#{hour}:#{minute}"
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" class="app-wrapper app-root">
|
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view></router-view>
|
<router-view />
|
||||||
</transition>
|
</transition>
|
||||||
<add-account-modal
|
<add-account-modal
|
||||||
:show="showAddAccountModal"
|
:show="showAddAccountModal"
|
||||||
|
@ -11,21 +11,28 @@
|
||||||
<woot-snackbar-box />
|
<woot-snackbar-box />
|
||||||
<network-notification />
|
<network-notification />
|
||||||
</div>
|
</div>
|
||||||
|
<loading-state v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { accountIdFromPathname } from './helper/URLHelper';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||||
|
import LoadingState from './components/widgets/LoadingState.vue';
|
||||||
import NetworkNotification from './components/NetworkNotification';
|
import NetworkNotification from './components/NetworkNotification';
|
||||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||||
|
import vueActionCable from './helper/actionCable';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer';
|
import WootSnackbarBox from './components/SnackbarContainer';
|
||||||
|
import {
|
||||||
|
registerSubscription,
|
||||||
|
verifyServiceWorkerExistence,
|
||||||
|
} from './helper/pushHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
|
LoadingState,
|
||||||
NetworkNotification,
|
NetworkNotification,
|
||||||
UpdateBanner,
|
UpdateBanner,
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
|
@ -43,13 +50,12 @@ export default {
|
||||||
getAccount: 'accounts/getAccount',
|
getAccount: 'accounts/getAccount',
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
|
authUIFlags: 'getAuthUIFlags',
|
||||||
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
hasAccounts() {
|
hasAccounts() {
|
||||||
return (
|
const { accounts = [] } = this.currentUser || {};
|
||||||
this.currentUser &&
|
return accounts.length > 0;
|
||||||
this.currentUser.accounts &&
|
|
||||||
this.currentUser.accounts.length !== 0
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -58,32 +64,37 @@ export default {
|
||||||
if (!this.hasAccounts) {
|
if (!this.hasAccounts) {
|
||||||
this.showAddAccountModal = true;
|
this.showAddAccountModal = true;
|
||||||
}
|
}
|
||||||
|
verifyServiceWorkerExistence(registration =>
|
||||||
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
|
if (subscription) {
|
||||||
|
registerSubscription();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
currentAccountId() {
|
||||||
|
if (this.currentAccountId) {
|
||||||
|
this.initializeAccount();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setUser');
|
|
||||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||||
this.initializeAccount();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setLocale(locale) {
|
setLocale(locale) {
|
||||||
this.$root.$i18n.locale = locale;
|
this.$root.$i18n.locale = locale;
|
||||||
},
|
},
|
||||||
|
|
||||||
async initializeAccount() {
|
async initializeAccount() {
|
||||||
const { pathname } = window.location;
|
await this.$store.dispatch('accounts/get');
|
||||||
const accountId = accountIdFromPathname(pathname);
|
const {
|
||||||
|
locale,
|
||||||
if (accountId) {
|
latest_chatwoot_version: latestChatwootVersion,
|
||||||
await this.$store.dispatch('accounts/get');
|
} = this.getAccount(this.currentAccountId);
|
||||||
const {
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||||
locale,
|
this.setLocale(locale);
|
||||||
latest_chatwoot_version: latestChatwootVersion,
|
this.latestChatwootVersion = latestChatwootVersion;
|
||||||
} = this.getAccount(accountId);
|
vueActionCable.init(pubsubToken);
|
||||||
this.setLocale(locale);
|
|
||||||
this.latestChatwootVersion = latestChatwootVersion;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
16
app/javascript/dashboard/api/assignableAgents.js
Normal file
16
app/javascript/dashboard/api/assignableAgents.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class AssignableAgents extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('assignable_agents', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(inboxIds) {
|
||||||
|
return axios.get(this.url, {
|
||||||
|
params: { inbox_ids: inboxIds },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AssignableAgents();
|
|
@ -1,6 +1,4 @@
|
||||||
/* eslint no-console: 0 */
|
|
||||||
/* global axios */
|
/* global axios */
|
||||||
/* eslint no-undef: "error" */
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import endPoints from './endPoints';
|
import endPoints from './endPoints';
|
||||||
|
@ -61,41 +59,15 @@ export default {
|
||||||
});
|
});
|
||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
},
|
},
|
||||||
|
hasAuthCookie() {
|
||||||
isLoggedIn() {
|
return !!Cookies.getJSON('cw_d_session_info');
|
||||||
const hasAuthCookie = !!Cookies.getJSON('auth_data');
|
|
||||||
const hasUserCookie = !!Cookies.getJSON('user');
|
|
||||||
return hasAuthCookie && hasUserCookie;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isAdmin() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
return Cookies.getJSON('user').role === 'administrator';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthData() {
|
getAuthData() {
|
||||||
if (this.isLoggedIn()) {
|
if (this.hasAuthCookie()) {
|
||||||
return Cookies.getJSON('auth_data');
|
return Cookies.getJSON('cw_d_session_info');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
getPubSubToken() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
const user = Cookies.getJSON('user') || {};
|
|
||||||
const { pubsub_token: pubsubToken } = user;
|
|
||||||
return pubsubToken;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
getCurrentUser() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
return Cookies.getJSON('user');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
verifyPasswordToken({ confirmationToken }) {
|
verifyPasswordToken({ confirmationToken }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios
|
axios
|
||||||
|
|
|
@ -9,6 +9,14 @@ class AutomationsAPI extends ApiClient {
|
||||||
clone(automationId) {
|
clone(automationId) {
|
||||||
return axios.post(`${this.url}/${automationId}/clone`);
|
return axios.post(`${this.url}/${automationId}/clone`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachment(file) {
|
||||||
|
return axios.post(`${this.url}/attach_file`, file, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AutomationsAPI();
|
export default new AutomationsAPI();
|
||||||
|
|
9
app/javascript/dashboard/api/bulkActions.js
Normal file
9
app/javascript/dashboard/api/bulkActions.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class BulkActionsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('bulk_actions', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BulkActionsAPI();
|
|
@ -18,6 +18,17 @@ class CSATReportsAPI extends ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
download({ from, to, user_ids } = {}) {
|
||||||
|
return axios.get(`${this.url}/download`, {
|
||||||
|
params: {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
sort: '-created_at',
|
||||||
|
user_ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getMetrics({ from, to, user_ids } = {}) {
|
getMetrics({ from, to, user_ids } = {}) {
|
||||||
return axios.get(`${this.url}/metrics`, {
|
return axios.get(`${this.url}/metrics`, {
|
||||||
params: { since: from, until: to, user_ids },
|
params: { since: from, until: to, user_ids },
|
||||||
|
|
9
app/javascript/dashboard/api/dashboardApps.js
Normal file
9
app/javascript/dashboard/api/dashboardApps.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class DashboardAppsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('dashboard_apps', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DashboardAppsAPI();
|
|
@ -10,6 +10,7 @@ export const buildCreatePayload = ({
|
||||||
files,
|
files,
|
||||||
ccEmails = '',
|
ccEmails = '',
|
||||||
bccEmails = '',
|
bccEmails = '',
|
||||||
|
templateParams,
|
||||||
}) => {
|
}) => {
|
||||||
let payload;
|
let payload;
|
||||||
if (files && files.length !== 0) {
|
if (files && files.length !== 0) {
|
||||||
|
@ -32,6 +33,7 @@ export const buildCreatePayload = ({
|
||||||
content_attributes: contentAttributes,
|
content_attributes: contentAttributes,
|
||||||
cc_emails: ccEmails,
|
cc_emails: ccEmails,
|
||||||
bcc_emails: bccEmails,
|
bcc_emails: bccEmails,
|
||||||
|
template_params: templateParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
@ -51,6 +53,7 @@ class MessageApi extends ApiClient {
|
||||||
files,
|
files,
|
||||||
ccEmails = '',
|
ccEmails = '',
|
||||||
bccEmails = '',
|
bccEmails = '',
|
||||||
|
templateParams,
|
||||||
}) {
|
}) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -63,6 +66,7 @@ class MessageApi extends ApiClient {
|
||||||
files,
|
files,
|
||||||
ccEmails,
|
ccEmails,
|
||||||
bccEmails,
|
bccEmails,
|
||||||
|
templateParams,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,6 @@ class Inboxes extends ApiClient {
|
||||||
super('inboxes', { accountScoped: true });
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssignableAgents(inboxId) {
|
|
||||||
return axios.get(`${this.url}/${inboxId}/assignable_agents`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCampaigns(inboxId) {
|
getCampaigns(inboxId) {
|
||||||
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,15 @@ class ReportsAPI extends ApiClient {
|
||||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
}
|
}
|
||||||
|
|
||||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
getReports(
|
||||||
|
metric,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
type = 'account',
|
||||||
|
id,
|
||||||
|
group_by,
|
||||||
|
business_hours
|
||||||
|
) {
|
||||||
return axios.get(`${this.url}`, {
|
return axios.get(`${this.url}`, {
|
||||||
params: {
|
params: {
|
||||||
metric,
|
metric,
|
||||||
|
@ -17,12 +25,13 @@ class ReportsAPI extends ApiClient {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by,
|
||||||
|
business_hours,
|
||||||
timezone_offset: getTimeOffset(),
|
timezone_offset: getTimeOffset(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary(since, until, type = 'account', id, group_by) {
|
getSummary(since, until, type = 'account', id, group_by, business_hours) {
|
||||||
return axios.get(`${this.url}/summary`, {
|
return axios.get(`${this.url}/summary`, {
|
||||||
params: {
|
params: {
|
||||||
since,
|
since,
|
||||||
|
@ -30,31 +39,41 @@ class ReportsAPI extends ApiClient {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by,
|
||||||
|
business_hours,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAgentReports(since, until) {
|
getConversationMetric(type = 'account', page = 1) {
|
||||||
|
return axios.get(`${this.url}/conversations`, {
|
||||||
|
params: {
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgentReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/agents`, {
|
return axios.get(`${this.url}/agents`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelReports(since, until) {
|
getLabelReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/labels`, {
|
return axios.get(`${this.url}/labels`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getInboxReports(since, until) {
|
getInboxReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/inboxes`, {
|
return axios.get(`${this.url}/inboxes`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTeamReports(since, until) {
|
getTeamReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/teams`, {
|
return axios.get(`${this.url}/teams`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
app/javascript/dashboard/api/specs/assignableAgents.spec.js
Normal file
18
app/javascript/dashboard/api/specs/assignableAgents.spec.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import assignableAgentsAPI from '../assignableAgents';
|
||||||
|
import describeWithAPIMock from './apiSpecHelper';
|
||||||
|
|
||||||
|
describe('#AssignableAgentsAPI', () => {
|
||||||
|
describeWithAPIMock('API calls', context => {
|
||||||
|
it('#getAssignableAgents', () => {
|
||||||
|
assignableAgentsAPI.get([1]);
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/assignable_agents',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
inbox_ids: [1],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
9
app/javascript/dashboard/api/specs/bulkAction.spec.js
Normal file
9
app/javascript/dashboard/api/specs/bulkAction.spec.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import bulkActions from '../bulkActions';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#BulkActionsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(bulkActions).toBeInstanceOf(ApiClient);
|
||||||
|
expect(bulkActions).toHaveProperty('create');
|
||||||
|
});
|
||||||
|
});
|
|
@ -33,5 +33,23 @@ describe('#Reports API', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('#download', () => {
|
||||||
|
csatReportsAPI.download({
|
||||||
|
from: 1622485800,
|
||||||
|
to: 1623695400,
|
||||||
|
user_ids: 1,
|
||||||
|
});
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/csat_survey_responses/download',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
since: 1622485800,
|
||||||
|
until: 1623695400,
|
||||||
|
user_ids: 1,
|
||||||
|
sort: '-created_at',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
13
app/javascript/dashboard/api/specs/dashboardApps.spec.js
Normal file
13
app/javascript/dashboard/api/specs/dashboardApps.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import dashboardAppsAPI from '../dashboardApps';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#dashboardAppsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(dashboardAppsAPI).toBeInstanceOf(ApiClient);
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('get');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('show');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('create');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('update');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,17 +10,9 @@ describe('#InboxesAPI', () => {
|
||||||
expect(inboxesAPI).toHaveProperty('create');
|
expect(inboxesAPI).toHaveProperty('create');
|
||||||
expect(inboxesAPI).toHaveProperty('update');
|
expect(inboxesAPI).toHaveProperty('update');
|
||||||
expect(inboxesAPI).toHaveProperty('delete');
|
expect(inboxesAPI).toHaveProperty('delete');
|
||||||
expect(inboxesAPI).toHaveProperty('getAssignableAgents');
|
|
||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getAssignableAgents', () => {
|
|
||||||
inboxesAPI.getAssignableAgents(1);
|
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/inboxes/1/assignable_agents'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('#getCampaigns', () => {
|
it('#getCampaigns', () => {
|
||||||
inboxesAPI.getCampaigns(2);
|
inboxesAPI.getCampaigns(2);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
|
|
@ -47,20 +47,25 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getAgentReports', () => {
|
it('#getAgentReports', () => {
|
||||||
reportsAPI.getAgentReports(1621103400, 1621621800);
|
reportsAPI.getAgentReports({
|
||||||
|
from: 1621103400,
|
||||||
|
to: 1621621800,
|
||||||
|
businessHours: true,
|
||||||
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/agents',
|
'/api/v2/reports/agents',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
since: 1621103400,
|
since: 1621103400,
|
||||||
until: 1621621800,
|
until: 1621621800,
|
||||||
|
business_hours: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getLabelReports', () => {
|
it('#getLabelReports', () => {
|
||||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/labels',
|
'/api/v2/reports/labels',
|
||||||
{
|
{
|
||||||
|
@ -73,7 +78,7 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getInboxReports', () => {
|
it('#getInboxReports', () => {
|
||||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/inboxes',
|
'/api/v2/reports/inboxes',
|
||||||
{
|
{
|
||||||
|
@ -86,7 +91,7 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getTeamReports', () => {
|
it('#getTeamReports', () => {
|
||||||
reportsAPI.getTeamReports(1621103400, 1621621800);
|
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/teams',
|
'/api/v2/reports/teams',
|
||||||
{
|
{
|
||||||
|
@ -97,5 +102,18 @@ describe('#Reports API', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#getConversationMetric', () => {
|
||||||
|
reportsAPI.getConversationMetric('account');
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v2/reports/conversations',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
type: 'account',
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -383,8 +383,8 @@ $form-button-radius: var(--border-radius-normal);
|
||||||
// 20. Label
|
// 20. Label
|
||||||
// ---------
|
// ---------
|
||||||
|
|
||||||
$label-background: $primary-color;
|
$label-background: $white;
|
||||||
$label-color: $white;
|
$label-color: $black;
|
||||||
$label-color-alt: $black;
|
$label-color-alt: $black;
|
||||||
$label-palette: $foundation-palette;
|
$label-palette: $foundation-palette;
|
||||||
$label-font-size: $font-size-mini;
|
$label-font-size: $font-size-mini;
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
margin-right: var(--space-small);
|
margin-right: var(--space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-bottom-small {
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
.margin-right-smaller {
|
.margin-right-smaller {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
@ -51,12 +55,14 @@
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-y-800 {
|
|
||||||
color: var(--y-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,16 @@
|
||||||
padding: 0 $space-small;
|
padding: 0 $space-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-js {
|
||||||
|
background: transparent;
|
||||||
|
// Override min-height : 50px in foundation
|
||||||
|
//
|
||||||
|
max-height: $space-mega * 2.4;
|
||||||
|
min-height: 4.8rem;
|
||||||
|
padding: var(--space-normal) 0 0;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
>textarea {
|
>textarea {
|
||||||
@include ghost-input();
|
@include ghost-input();
|
||||||
@include margin(0);
|
@include margin(0);
|
||||||
|
|
|
@ -78,5 +78,10 @@
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-hours {
|
||||||
|
margin: $space-normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,3 +25,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-hours {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-hours-text {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="conversations-list-wrap">
|
<div class="conversations-list-wrap">
|
||||||
<slot></slot>
|
<slot />
|
||||||
<div
|
<div
|
||||||
class="chat-list__top"
|
class="chat-list__top"
|
||||||
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
||||||
|
@ -53,8 +53,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
class="btn-filter"
|
class="btn-filter"
|
||||||
@click="onToggleAdvanceFiltersModal"
|
@click="onToggleAdvanceFiltersModal"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -84,7 +83,19 @@
|
||||||
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
|
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
|
||||||
{{ $t('CHAT_LIST.LIST.404') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
|
<conversation-bulk-actions
|
||||||
|
v-if="selectedConversations.length"
|
||||||
|
:conversations="selectedConversations"
|
||||||
|
:all-conversations-selected="allConversationsSelected"
|
||||||
|
:selected-inboxes="uniqueInboxes"
|
||||||
|
:show-open-action="allSelectedConversationsStatus('open')"
|
||||||
|
:show-resolved-action="allSelectedConversationsStatus('resolved')"
|
||||||
|
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
|
||||||
|
@select-all-conversations="selectAllConversations"
|
||||||
|
@assign-agent="onAssignAgent"
|
||||||
|
@update-conversations="onUpdateConversations"
|
||||||
|
@assign-labels="onAssignLabels"
|
||||||
|
/>
|
||||||
<div ref="activeConversation" class="conversations-list">
|
<div ref="activeConversation" class="conversations-list">
|
||||||
<conversation-card
|
<conversation-card
|
||||||
v-for="chat in conversationList"
|
v-for="chat in conversationList"
|
||||||
|
@ -95,10 +106,13 @@
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
:conversation-type="conversationType"
|
:conversation-type="conversationType"
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
:show-assignee="showAssigneeInConversationCard"
|
||||||
|
:selected="isConversationSelected(chat.id)"
|
||||||
|
@select-conversation="selectConversation"
|
||||||
|
@de-select-conversation="deSelectConversation"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
<div v-if="chatListLoading" class="text-center">
|
||||||
<span class="spinner"></span>
|
<span class="spinner" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<woot-button
|
<woot-button
|
||||||
|
@ -111,11 +125,7 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="showEndOfListMessage"
|
||||||
conversationList.length &&
|
|
||||||
hasCurrentPageEndReached &&
|
|
||||||
!chatListLoading
|
|
||||||
"
|
|
||||||
class="text-center text-muted end-of-list-text"
|
class="text-center text-muted end-of-list-text"
|
||||||
>
|
>
|
||||||
{{ $t('CHAT_LIST.EOF') }}
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
|
@ -151,6 +161,8 @@ import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||||
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||||
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasPressedAltAndJKey,
|
hasPressedAltAndJKey,
|
||||||
|
@ -165,8 +177,9 @@ export default {
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
ConversationAdvancedFilter,
|
ConversationAdvancedFilter,
|
||||||
DeleteCustomViews,
|
DeleteCustomViews,
|
||||||
|
ConversationBulkActions,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
|
@ -201,6 +214,8 @@ export default {
|
||||||
foldersQuery: {},
|
foldersQuery: {},
|
||||||
showAddFoldersModal: false,
|
showAddFoldersModal: false,
|
||||||
showDeleteFoldersModal: false,
|
showDeleteFoldersModal: false,
|
||||||
|
selectedConversations: [],
|
||||||
|
selectedInboxes: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -216,6 +231,7 @@ export default {
|
||||||
conversationStats: 'conversationStats/getStats',
|
conversationStats: 'conversationStats/getStats',
|
||||||
appliedFilters: 'getAppliedConversationFilters',
|
appliedFilters: 'getAppliedConversationFilters',
|
||||||
folders: 'customViews/getCustomViews',
|
folders: 'customViews/getCustomViews',
|
||||||
|
inboxes: 'inboxes/getInboxes',
|
||||||
}),
|
}),
|
||||||
hasAppliedFilters() {
|
hasAppliedFilters() {
|
||||||
return this.appliedFilters.length !== 0;
|
return this.appliedFilters.length !== 0;
|
||||||
|
@ -233,12 +249,24 @@ export default {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
showEndOfListMessage() {
|
||||||
|
return (
|
||||||
|
this.conversationList.length &&
|
||||||
|
this.hasCurrentPageEndReached &&
|
||||||
|
!this.chatListLoading
|
||||||
|
);
|
||||||
|
},
|
||||||
assigneeTabItems() {
|
assigneeTabItems() {
|
||||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
const ASSIGNEE_TYPE_TAB_KEYS = {
|
||||||
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
me: 'mineCount',
|
||||||
|
unassigned: 'unAssignedCount',
|
||||||
|
all: 'allCount',
|
||||||
|
};
|
||||||
|
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
|
||||||
|
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
|
||||||
return {
|
return {
|
||||||
key: item.KEY,
|
key,
|
||||||
name: item.NAME,
|
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
||||||
count,
|
count,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -337,6 +365,17 @@ export default {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
allConversationsSelected() {
|
||||||
|
return (
|
||||||
|
this.conversationList.length === this.selectedConversations.length &&
|
||||||
|
this.conversationList.every(el =>
|
||||||
|
this.selectedConversations.includes(el.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
uniqueInboxes() {
|
||||||
|
return [...new Set(this.selectedInboxes)];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
activeTeam() {
|
activeTeam() {
|
||||||
|
@ -370,6 +409,7 @@ export default {
|
||||||
if (this.$route.name !== 'home') {
|
if (this.$route.name !== 'home') {
|
||||||
this.$router.push({ name: 'home' });
|
this.$router.push({ name: 'home' });
|
||||||
}
|
}
|
||||||
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
@ -435,6 +475,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetAndFetchData() {
|
resetAndFetchData() {
|
||||||
|
this.resetBulkActions();
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
this.$store.dispatch('clearConversationFilters');
|
this.$store.dispatch('clearConversationFilters');
|
||||||
|
@ -485,6 +526,7 @@ export default {
|
||||||
},
|
},
|
||||||
updateAssigneeTab(selectedTab) {
|
updateAssigneeTab(selectedTab) {
|
||||||
if (this.activeAssigneeTab !== selectedTab) {
|
if (this.activeAssigneeTab !== selectedTab) {
|
||||||
|
this.resetBulkActions();
|
||||||
bus.$emit('clearSearchInput');
|
bus.$emit('clearSearchInput');
|
||||||
this.activeAssigneeTab = selectedTab;
|
this.activeAssigneeTab = selectedTab;
|
||||||
if (!this.currentPage) {
|
if (!this.currentPage) {
|
||||||
|
@ -492,6 +534,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
resetBulkActions() {
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.selectedInboxes = [];
|
||||||
|
},
|
||||||
updateStatusType(index) {
|
updateStatusType(index) {
|
||||||
if (this.activeStatus !== index) {
|
if (this.activeStatus !== index) {
|
||||||
this.activeStatus = index;
|
this.activeStatus = index;
|
||||||
|
@ -514,6 +560,80 @@ export default {
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isConversationSelected(id) {
|
||||||
|
return this.selectedConversations.includes(id);
|
||||||
|
},
|
||||||
|
selectConversation(conversationId, inboxId) {
|
||||||
|
this.selectedConversations.push(conversationId);
|
||||||
|
this.selectedInboxes.push(inboxId);
|
||||||
|
},
|
||||||
|
deSelectConversation(conversationId, inboxId) {
|
||||||
|
this.selectedConversations = this.selectedConversations.filter(
|
||||||
|
item => item !== conversationId
|
||||||
|
);
|
||||||
|
this.selectedInboxes = this.selectedInboxes.filter(
|
||||||
|
item => item !== inboxId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectAllConversations(check) {
|
||||||
|
if (check) {
|
||||||
|
this.selectedConversations = this.conversationList.map(item => item.id);
|
||||||
|
this.selectedInboxes = this.conversationList.map(item => item.inbox_id);
|
||||||
|
} else {
|
||||||
|
this.resetBulkActions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onAssignAgent(agent) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
assignee_id: agent.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onAssignLabels(labels) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
labels: {
|
||||||
|
add: labels,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onUpdateConversations(status) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allSelectedConversationsStatus(status) {
|
||||||
|
if (!this.selectedConversations.length) return false;
|
||||||
|
return this.selectedConversations.every(item => {
|
||||||
|
return this.$store.getters.getConversationById(item).status === status;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -529,7 +649,7 @@ export default {
|
||||||
.conversations-list-wrap {
|
.conversations-list-wrap {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 34rem;
|
width: 34rem;
|
||||||
|
overflow: hidden;
|
||||||
@include breakpoint(large up) {
|
@include breakpoint(large up) {
|
||||||
width: 36rem;
|
width: 36rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,4 +98,7 @@ export default {
|
||||||
width: 48rem;
|
width: 48rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modal-big {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<p v-if="headerContent" class="small-12 column">
|
<p v-if="headerContent" class="small-12 column">
|
||||||
{{ headerContent }}
|
{{ headerContent }}
|
||||||
</p>
|
</p>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
color-scheme="warning"
|
color-scheme="warning"
|
||||||
icon="dismiss-circle"
|
icon="dismiss-circle"
|
||||||
@click="closeNotification"
|
@click="closeNotification"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
<p class="sub-head">
|
<p class="sub-head">
|
||||||
{{ subTitle }}
|
{{ subTitle }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="note">
|
||||||
|
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||||
|
{{ note }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-6 small-12">
|
<div class="medium-6 small-12">
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,6 +29,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
note: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,5 +54,9 @@ export default {
|
||||||
.title--section {
|
.title--section {
|
||||||
padding-right: var(--space-large);
|
padding-right: var(--space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
/>
|
/>
|
||||||
<spinner v-if="isLoading" />
|
<spinner v-if="isLoading" />
|
||||||
<slot></slot>
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -57,3 +57,13 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
button:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--w-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--w-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||||
getCurrentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
availabilityDisplayLabel() {
|
availabilityDisplayLabel() {
|
||||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||||
|
@ -63,9 +63,6 @@ export default {
|
||||||
availabilityIndex
|
availabilityIndex
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
currentAccountId() {
|
|
||||||
return this.getCurrentAccountId;
|
|
||||||
},
|
|
||||||
currentUserAvailability() {
|
currentUserAvailability() {
|
||||||
return this.getCurrentUserAvailability;
|
return this.getCurrentUserAvailability;
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
:menu-config="activeSecondaryMenu"
|
:menu-config="activeSecondaryMenu"
|
||||||
:current-role="currentRole"
|
:current-role="currentRole"
|
||||||
@add-label="showAddLabelPopup"
|
@add-label="showAddLabelPopup"
|
||||||
|
@toggle-accounts="toggleAccountModal"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
const reports = accountId => ({
|
const reports = accountId => ({
|
||||||
parentNav: 'reports',
|
parentNav: 'reports',
|
||||||
routes: [
|
routes: [
|
||||||
'settings_account_reports',
|
'account_overview_reports',
|
||||||
|
'conversation_reports',
|
||||||
'csat_reports',
|
'csat_reports',
|
||||||
'agent_reports',
|
'agent_reports',
|
||||||
'label_reports',
|
'label_reports',
|
||||||
|
@ -16,7 +17,14 @@ const reports = accountId => ({
|
||||||
label: 'REPORTS_OVERVIEW',
|
label: 'REPORTS_OVERVIEW',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||||
toStateName: 'settings_account_reports',
|
toStateName: 'account_overview_reports',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'chat',
|
||||||
|
label: 'REPORTS_CONVERSATION',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
|
||||||
|
toStateName: 'conversation_reports',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'emoji',
|
icon: 'emoji',
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="showShowCurrentAccountContext" class="account-context--group">
|
<div
|
||||||
|
v-if="showShowCurrentAccountContext"
|
||||||
|
class="account-context--group"
|
||||||
|
@mouseover="setShowSwitch"
|
||||||
|
@mouseleave="resetShowSwitch"
|
||||||
|
>
|
||||||
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
||||||
<p class="account-context--name text-ellipsis">
|
<p class="account-context--name text-ellipsis">
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</p>
|
</p>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="showSwitchButton" class="account-context--switch-group">
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
icon="arrow-swap"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="$emit('toggle-accounts')"
|
||||||
|
>
|
||||||
|
{{ $t('SIDEBAR.SWITCH') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return { showSwitchButton: false };
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
account: 'getCurrentAccount',
|
account: 'getCurrentAccount',
|
||||||
|
@ -18,6 +38,14 @@ export default {
|
||||||
return this.userAccounts.length > 1 && this.account.name;
|
return this.userAccounts.length > 1 && this.account.name;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
setShowSwitch() {
|
||||||
|
this.showSwitchButton = true;
|
||||||
|
},
|
||||||
|
resetShowSwitch() {
|
||||||
|
this.showSwitchButton = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@ -27,10 +55,49 @@ export default {
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
padding: var(--space-small);
|
padding: var(--space-small);
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-small);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--b-100);
|
||||||
|
}
|
||||||
|
|
||||||
.account-context--name {
|
.account-context--name {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-context--switch-group {
|
||||||
|
--overlay-shadow: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 1) 50%
|
||||||
|
);
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
background-image: var(--overlay-shadow);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: var(--border-radius-normal);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: var(--border-radius-normal);
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: end;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
|
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
|
||||||
<account-context />
|
<account-context @toggle-accounts="toggleAccountModal" />
|
||||||
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
||||||
<secondary-nav-item
|
<secondary-nav-item
|
||||||
v-for="menuItem in accessibleMenuItems"
|
v-for="menuItem in accessibleMenuItems"
|
||||||
|
@ -85,14 +85,21 @@ export default {
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||||
toStateName: 'settings_inbox_new',
|
toStateName: 'settings_inbox_new',
|
||||||
newLinkRouteName: 'settings_inbox_new',
|
newLinkRouteName: 'settings_inbox_new',
|
||||||
children: this.inboxes.map(inbox => ({
|
children: this.inboxes
|
||||||
id: inbox.id,
|
.map(inbox => ({
|
||||||
label: inbox.name,
|
id: inbox.id,
|
||||||
truncateLabel: true,
|
label: inbox.name,
|
||||||
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
|
truncateLabel: true,
|
||||||
type: inbox.channel_type,
|
toState: frontendURL(
|
||||||
phoneNumber: inbox.phone_number,
|
`accounts/${this.accountId}/inbox/${inbox.id}`
|
||||||
})),
|
),
|
||||||
|
type: inbox.channel_type,
|
||||||
|
phoneNumber: inbox.phone_number,
|
||||||
|
reauthorizationRequired: inbox.reauthorization_required,
|
||||||
|
}))
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
labelSection() {
|
labelSection() {
|
||||||
|
@ -218,6 +225,9 @@ export default {
|
||||||
showAddLabelPopup() {
|
showAddLabelPopup() {
|
||||||
this.$emit('add-label');
|
this.$emit('add-label');
|
||||||
},
|
},
|
||||||
|
toggleAccountModal() {
|
||||||
|
this.$emit('toggle-accounts');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -30,6 +30,14 @@
|
||||||
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="warningIcon" class="badge--icon">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
|
||||||
|
class="inbox-icon"
|
||||||
|
:icon="warningIcon"
|
||||||
|
size="12"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -57,6 +65,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
warningIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
count: {
|
count: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
:label-color="child.color"
|
:label-color="child.color"
|
||||||
:should-truncate="child.truncateLabel"
|
:should-truncate="child.truncateLabel"
|
||||||
:icon="computedInboxClass(child)"
|
:icon="computedInboxClass(child)"
|
||||||
|
:warning-icon="computedInboxErrorClass(child)"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showItem(menuItem)"
|
v-if="showItem(menuItem)"
|
||||||
|
@ -63,7 +64,10 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import adminMixin from '../../../mixins/isAdmin';
|
import adminMixin from '../../../mixins/isAdmin';
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import {
|
||||||
|
getInboxClassByType,
|
||||||
|
getInboxWarningIconClass,
|
||||||
|
} from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
||||||
|
|
||||||
|
@ -136,6 +140,15 @@ export default {
|
||||||
const classByType = getInboxClassByType(type, phoneNumber);
|
const classByType = getInboxClassByType(type, phoneNumber);
|
||||||
return classByType;
|
return classByType;
|
||||||
},
|
},
|
||||||
|
computedInboxErrorClass(child) {
|
||||||
|
const { type, reauthorizationRequired } = child;
|
||||||
|
if (!type) return '';
|
||||||
|
const warningClass = getInboxWarningIconClass(
|
||||||
|
type,
|
||||||
|
reauthorizationRequired
|
||||||
|
);
|
||||||
|
return warningClass;
|
||||||
|
},
|
||||||
newLinkClick(e, navigate) {
|
newLinkClick(e, navigate) {
|
||||||
if (this.menuItem.newLinkRouteName) {
|
if (this.menuItem.newLinkRouteName) {
|
||||||
navigate(e);
|
navigate(e);
|
||||||
|
|
|
@ -30,8 +30,7 @@
|
||||||
icon="dismiss-circle"
|
icon="dismiss-circle"
|
||||||
class-names="banner-action__button"
|
class-names="banner-action__button"
|
||||||
@click="onClickClose"
|
@click="onClickClose"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -112,10 +111,10 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: var(--y-800);
|
background: var(--y-600);
|
||||||
color: var(--s-600);
|
color: var(--y-500);
|
||||||
a {
|
a {
|
||||||
color: var(--s-600);
|
color: var(--y-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="labelClass" :style="labelStyle" :title="description">
|
<div :class="labelClass" :style="labelStyle" :title="description">
|
||||||
<button v-if="icon" class="label-action--button" @click="onClick">
|
<span v-if="icon" class="label-action--button">
|
||||||
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
||||||
</button>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="variant === 'smooth'"
|
||||||
|
:style="{ background: color }"
|
||||||
|
class="label-color-dot"
|
||||||
|
/>
|
||||||
<span v-if="!href" class="label__title">{{ title }}</span>
|
<span v-if="!href" class="label__title">{{ title }}</span>
|
||||||
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
||||||
<button
|
<button
|
||||||
v-if="showClose"
|
v-if="showClose"
|
||||||
class="label-action--button"
|
class="label-close--button "
|
||||||
:style="{ color: textColor }"
|
:style="{ color: textColor }"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
|
@ -56,9 +61,14 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
textColor() {
|
textColor() {
|
||||||
|
if (this.variant === 'smooth') return '';
|
||||||
return this.color || getContrastingTextColor(this.bgColor);
|
return this.color || getContrastingTextColor(this.bgColor);
|
||||||
},
|
},
|
||||||
labelClass() {
|
labelClass() {
|
||||||
|
@ -98,6 +108,11 @@ export default {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
margin-bottom: var(--space-smaller);
|
margin-bottom: var(--space-smaller);
|
||||||
|
padding: var(--space-smaller);
|
||||||
|
background: var(--s-50);
|
||||||
|
color: var(--s-800);
|
||||||
|
border: 1px solid var(--s-75);
|
||||||
|
height: var(--space-medium);
|
||||||
|
|
||||||
text-shadow: 0.1px 0 rgb(0 0 0 / 5%), 0 0.1px rgb(0 0 0 / 5%),
|
text-shadow: 0.1px 0 rgb(0 0 0 / 5%), 0 0.1px rgb(0 0 0 / 5%),
|
||||||
-0.1px 0 rgb(0 0 0 / 5%), 0 -0.1px rgb(0 0 0 / 5%);
|
-0.1px 0 rgb(0 0 0 / 5%), 0 -0.1px rgb(0 0 0 / 5%);
|
||||||
|
@ -115,11 +130,6 @@ export default {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close--icon {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.small .label--icon,
|
&.small .label--icon,
|
||||||
&.small .close--icon {
|
&.small .close--icon {
|
||||||
font-size: var(--font-size-nano);
|
font-size: var(--font-size-nano);
|
||||||
|
@ -165,13 +175,28 @@ export default {
|
||||||
&.warning {
|
&.warning {
|
||||||
background: var(--y-100);
|
background: var(--y-100);
|
||||||
color: var(--y-900);
|
color: var(--y-900);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--y-900);
|
color: var(--y-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-close--button {
|
||||||
|
color: var(--s-800);
|
||||||
|
margin-bottom: var(--space-minus-micro);
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--s-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.label-action--button {
|
.label-action--button {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -183,4 +208,12 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-color-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--space-one);
|
||||||
|
height: var(--space-one);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,54 +1,66 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="switch" :class="classObject">
|
<button
|
||||||
<input
|
type="button"
|
||||||
:id="id"
|
class="toggle-button"
|
||||||
v-model="value"
|
:class="{ active: value }"
|
||||||
class="switch-input"
|
role="switch"
|
||||||
:name="name"
|
:aria-checked="value.toString()"
|
||||||
:disabled="disabled"
|
@click="onClick"
|
||||||
type="checkbox"
|
>
|
||||||
/>
|
<span aria-hidden="true" :class="{ active: value }" />
|
||||||
<div class="switch-paddle" :for="name">
|
</button>
|
||||||
<span class="show-for-sr">on off</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
disabled: Boolean,
|
value: { type: Boolean, default: false },
|
||||||
type: { type: String, default: '' },
|
|
||||||
size: { type: String, default: '' },
|
|
||||||
checked: Boolean,
|
|
||||||
name: { type: String, default: '' },
|
|
||||||
id: { type: String, default: '' },
|
|
||||||
},
|
},
|
||||||
data() {
|
methods: {
|
||||||
return {
|
onClick() {
|
||||||
value: null,
|
this.$emit('input', !this.value);
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
classObject() {
|
|
||||||
const { type, size, value } = this;
|
|
||||||
return {
|
|
||||||
[`is-${type}`]: type,
|
|
||||||
[`${size}`]: size,
|
|
||||||
checked: value,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
this.$emit('input', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
this.value = this.checked;
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$emit('input', (this.value = !!this.checked));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.toggle-button {
|
||||||
|
--toggle-button-box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||||
|
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||||
|
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||||
|
background-color: var(--s-200);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 19px;
|
||||||
|
position: relative;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: 34px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
--space-one-point-five: 1.5rem;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: var(--toggle-button-box-shadow);
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--space-one-point-five);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: transform;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: var(--space-one-point-five);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:icon-size="iconSize"
|
:icon-size="iconSize"
|
||||||
/>
|
/>
|
||||||
<span v-if="$slots.default" class="button__content"><slot></slot></span>
|
<span v-if="$slots.default" class="button__content"><slot /></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<select
|
<select
|
||||||
v-model="action_name"
|
v-model="action_name"
|
||||||
class="action__question"
|
class="action__question"
|
||||||
:class="{ 'full-width': !inputType }"
|
:class="{ 'full-width': !showActionInput }"
|
||||||
@change="resetAction()"
|
@change="resetAction()"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
{{ attribute.label }}
|
{{ attribute.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="filter__answer--wrap">
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
<div v-if="inputType">
|
<div v-if="inputType">
|
||||||
<div
|
<div
|
||||||
v-if="inputType === 'multi_select'"
|
v-if="inputType === 'multi_select'"
|
||||||
|
@ -52,6 +52,11 @@
|
||||||
class="answer--text-input"
|
class="answer--text-input"
|
||||||
placeholder="Enter url"
|
placeholder="Enter url"
|
||||||
/>
|
/>
|
||||||
|
<automation-action-file-input
|
||||||
|
v-if="inputType === 'attachment'"
|
||||||
|
v-model="action_params"
|
||||||
|
:initial-file-name="initialFileName"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
@ -61,6 +66,18 @@
|
||||||
@click="removeAction"
|
@click="removeAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<automation-action-team-message-input
|
||||||
|
v-if="inputType === 'team_message'"
|
||||||
|
v-model="action_params"
|
||||||
|
:teams="dropdownValues"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-if="inputType === 'textarea'"
|
||||||
|
v-model="action_params"
|
||||||
|
rows="4"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
class="action-message"
|
||||||
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||||
class="filter-error"
|
class="filter-error"
|
||||||
|
@ -71,7 +88,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
|
||||||
|
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AutomationActionTeamMessageInput,
|
||||||
|
AutomationActionFileInput,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -89,6 +112,14 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null,
|
default: () => null,
|
||||||
},
|
},
|
||||||
|
showActionInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
initialFileName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
|
@ -167,6 +198,7 @@ export default {
|
||||||
.filter__answer--wrap {
|
.filter__answer--wrap {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
max-width: 50%;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -207,4 +239,7 @@ export default {
|
||||||
.multiselect {
|
.multiselect {
|
||||||
margin-bottom: var(--space-zero);
|
margin-bottom: var(--space-zero);
|
||||||
}
|
}
|
||||||
|
.action-message {
|
||||||
|
margin: var(--space-small) 0 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedTeams"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
|
||||||
|
:multiple="true"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="teams"
|
||||||
|
:allow-empty="false"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="message"
|
||||||
|
rows="4"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedTeams: [],
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const { team_ids: teamIds } = this.value;
|
||||||
|
this.selectedTeams = teamIds;
|
||||||
|
this.message = this.value.message;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateValue() {
|
||||||
|
this.$emit('input', {
|
||||||
|
team_ids: this.selectedTeams.map(team => team.id),
|
||||||
|
message: this.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.multiselect {
|
||||||
|
margin: var(--space-smaller) var(--space-zero);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<label class="input-wrapper" :class="uploadState">
|
||||||
|
<input
|
||||||
|
v-if="uploadState !== 'processing'"
|
||||||
|
type="file"
|
||||||
|
name="attachment"
|
||||||
|
:class="uploadState === 'processing' ? 'disabled' : ''"
|
||||||
|
@change="onChangeFile"
|
||||||
|
/>
|
||||||
|
<spinner v-if="uploadState === 'processing'" />
|
||||||
|
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
|
||||||
|
<fluent-icon
|
||||||
|
v-if="uploadState === 'uploaded'"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="outline"
|
||||||
|
class="success-icon"
|
||||||
|
/>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="uploadState === 'failed'"
|
||||||
|
icon="dismiss-circle"
|
||||||
|
type="outline"
|
||||||
|
class="error-icon"
|
||||||
|
/>
|
||||||
|
<p class="file-button">{{ label }}</p>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
initialFileName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploadState: 'idle',
|
||||||
|
label: this.$t('AUTOMATION.ATTACHMENT.LABEL_IDLE'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.initialFileName) {
|
||||||
|
this.label = this.initialFileName;
|
||||||
|
this.uploadState = 'uploaded';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onChangeFile(event) {
|
||||||
|
this.uploadState = 'processing';
|
||||||
|
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
|
||||||
|
try {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachment', file, file.name);
|
||||||
|
const id = await this.$store.dispatch(
|
||||||
|
'automations/uploadAttachment',
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
this.$emit('input', [id]);
|
||||||
|
this.uploadState = 'uploaded';
|
||||||
|
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADED');
|
||||||
|
} catch (error) {
|
||||||
|
this.uploadState = 'failed';
|
||||||
|
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
|
||||||
|
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
input[type='file'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 39px;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
border: 1px dashed var(--w-100);
|
||||||
|
padding: var(--space-smaller) var(--space-small);
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.success-icon {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
color: var(--g-500);
|
||||||
|
}
|
||||||
|
.error-icon {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
color: var(--r-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.file-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard-app--container">
|
||||||
|
<div
|
||||||
|
v-for="(configItem, index) in config"
|
||||||
|
:key="index"
|
||||||
|
class="dashboard-app--list"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
v-if="configItem.type === 'frame' && configItem.url"
|
||||||
|
:id="`dashboard-app--frame-${index}`"
|
||||||
|
:src="configItem.url"
|
||||||
|
@load="() => onIframeLoad(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
currentChat: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dashboardAppContext() {
|
||||||
|
return {
|
||||||
|
conversation: this.currentChat,
|
||||||
|
contact: this.$store.getters['contacts/getContact'](this.contactId),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contactId() {
|
||||||
|
return this.currentChat?.meta?.sender?.id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onIframeLoad(index) {
|
||||||
|
const frameElement = document.getElementById(
|
||||||
|
`dashboard-app--frame-${index}`
|
||||||
|
);
|
||||||
|
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||||
|
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-app--container,
|
||||||
|
.dashboard-app--list,
|
||||||
|
.dashboard-app--list iframe {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app--list iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -215,6 +215,32 @@ export default {
|
||||||
this.$emit('input', { ...payload, query_operator: value });
|
this.$emit('input', { ...payload, query_operator: value });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
custom_attribute_type: {
|
||||||
|
get() {
|
||||||
|
if (!this.customAttributeType) return '';
|
||||||
|
return this.customAttributeType;
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', {
|
||||||
|
...payload,
|
||||||
|
custom_attribute_type: this.customAttributeType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
customAttributeType: {
|
||||||
|
handler(value) {
|
||||||
|
if (
|
||||||
|
value === 'conversation_attribute' ||
|
||||||
|
value === 'contact_attribute'
|
||||||
|
) {
|
||||||
|
this.value.custom_attribute_type = this.customAttributeType;
|
||||||
|
} else this.value.custom_attribute_type = '';
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeFilter() {
|
removeFilter() {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
:title="label.title"
|
:title="label.title"
|
||||||
:description="label.description"
|
:description="label.description"
|
||||||
:show-close="true"
|
:show-close="true"
|
||||||
:bg-color="getBleachBgOfHexColor(label.color)"
|
|
||||||
:color="label.color"
|
:color="label.color"
|
||||||
|
variant="smooth"
|
||||||
@click="removeItem"
|
@click="removeItem"
|
||||||
/>
|
/>
|
||||||
<div class="dropdown-wrap">
|
<div class="dropdown-wrap">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<p v-if="headerContent" class="small-12 column">
|
<p v-if="headerContent" class="small-12 column">
|
||||||
{{ headerContent }}
|
{{ headerContent }}
|
||||||
</p>
|
</p>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
{{ textToBeDisplayed }}
|
||||||
|
<button class="show-more--button" @click="toggleShowMore">
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMore: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
textToBeDisplayed() {
|
||||||
|
if (this.showMore) {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.text.slice(0, this.limit) + '...';
|
||||||
|
},
|
||||||
|
buttonLabel() {
|
||||||
|
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
|
||||||
|
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowMore() {
|
||||||
|
this.showMore = !this.showMore;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.show-more--button {
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -220,7 +220,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-online-status--busy {
|
.user-online-status--busy {
|
||||||
background: var(--y-700);
|
background: var(--y-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-online-status--offline {
|
.user-online-status--offline {
|
||||||
|
|
|
@ -1,16 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="audio-wave-wrapper">
|
<div class="audio-wave-wrapper">
|
||||||
<div id="audio-wave"></div>
|
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
import 'video.js/dist/video-js.css';
|
||||||
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
import 'videojs-record/dist/css/videojs.record.css';
|
||||||
import RecordRTC from 'recordrtc';
|
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
||||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
import alertMixin from '../../../../shared/mixins/alertMixin';
|
||||||
|
|
||||||
|
import Recorder from 'opus-recorder';
|
||||||
|
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
|
||||||
|
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
||||||
|
import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
|
||||||
|
|
||||||
|
import 'videojs-record/dist/videojs.record.js';
|
||||||
|
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
|
||||||
|
import { format, addSeconds } from 'date-fns';
|
||||||
|
|
||||||
WaveSurfer.microphone = MicrophonePlugin;
|
WaveSurfer.microphone = MicrophonePlugin;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -18,104 +31,116 @@ export default {
|
||||||
mixins: [inboxMixin, alertMixin],
|
mixins: [inboxMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
wavesurfer: false,
|
player: false,
|
||||||
recorder: false,
|
recordingDateStarted: new Date(0),
|
||||||
recordingInterval: false,
|
|
||||||
recordingDateStarted: new Date().getTime(),
|
|
||||||
timeDuration: '00:00',
|
|
||||||
initialTimeDuration: '00:00',
|
initialTimeDuration: '00:00',
|
||||||
options: {
|
recorderOptions: {
|
||||||
container: '#audio-wave',
|
debug: true,
|
||||||
backend: 'WebAudio',
|
controls: true,
|
||||||
interact: true,
|
bigPlayButton: false,
|
||||||
cursorWidth: 1,
|
fluid: false,
|
||||||
plugins: [
|
controlBar: {
|
||||||
WaveSurfer.microphone.create({
|
deviceButton: false,
|
||||||
bufferSize: 4096,
|
fullscreenToggle: false,
|
||||||
numberOfInputChannels: 1,
|
cameraButton: false,
|
||||||
numberOfOutputChannels: 1,
|
volumePanel: false,
|
||||||
constraints: {
|
},
|
||||||
video: false,
|
plugins: {
|
||||||
audio: true,
|
wavesurfer: {
|
||||||
},
|
backend: 'WebAudio',
|
||||||
}),
|
waveColor: '#1f93ff',
|
||||||
],
|
progressColor: 'rgb(25, 118, 204)',
|
||||||
},
|
cursorColor: 'rgba(43, 51, 63, 0.7)',
|
||||||
optionsRecorder: {
|
backgroundColor: 'none',
|
||||||
type: 'audio',
|
barWidth: 1,
|
||||||
mimeType: 'audio/wav',
|
cursorWidth: 1,
|
||||||
disableLogs: true,
|
hideScrollbar: true,
|
||||||
recorderType: RecordRTC.StereoAudioRecorder,
|
plugins: [
|
||||||
sampleRate: 44100,
|
WaveSurfer.microphone.create({
|
||||||
numberOfAudioChannels: 2,
|
bufferSize: 4096,
|
||||||
checkForInactiveTracks: true,
|
numberOfInputChannels: 1,
|
||||||
bufferSize: 4096,
|
numberOfOutputChannels: 1,
|
||||||
|
constraints: {
|
||||||
|
video: false,
|
||||||
|
audio: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
displayMilliseconds: false,
|
||||||
|
maxLength: 300,
|
||||||
|
audioEngine: 'opus-recorder',
|
||||||
|
audioWorkerURL: encoderWorker,
|
||||||
|
audioChannels: 1,
|
||||||
|
audioSampleRate: 48000,
|
||||||
|
audioBitRate: 128,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isRecording() {
|
isRecording() {
|
||||||
if (this.recorder) {
|
return this.player && this.player.record().isRecording();
|
||||||
return this.recorder.getState() === 'recording';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.wavesurfer = WaveSurfer.create(this.options);
|
window.Recorder = Recorder;
|
||||||
this.wavesurfer.on('play', this.playingRecorder);
|
this.fireProgressRecord(this.initialTimeDuration);
|
||||||
this.wavesurfer.on('pause', this.pausedRecorder);
|
this.player = videojs('#audio-wave', this.recorderOptions, () => {
|
||||||
this.wavesurfer.microphone.on('deviceReady', this.startRecording);
|
this.$nextTick(() => {
|
||||||
this.wavesurfer.microphone.on('deviceError', this.deviceError);
|
this.player.record().getDevice();
|
||||||
this.wavesurfer.microphone.start();
|
});
|
||||||
this.fireStateRecorderTimerChanged(this.initialTimeDuration);
|
});
|
||||||
|
this.player.on('deviceReady', this.deviceReady);
|
||||||
|
this.player.on('deviceError', this.deviceError);
|
||||||
|
this.player.on('startRecord', this.startRecord);
|
||||||
|
this.player.on('stopRecord', this.stopRecord);
|
||||||
|
this.player.on('progressRecord', this.progressRecord);
|
||||||
|
this.player.on('finishRecord', this.finishRecord);
|
||||||
|
this.player.on('playbackFinish', this.playbackFinish);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.recorder) {
|
if (this.player) {
|
||||||
this.recorder.destroy();
|
this.player.dispose();
|
||||||
}
|
}
|
||||||
if (this.wavesurfer) {
|
if (window.Recorder) {
|
||||||
this.wavesurfer.destroy();
|
window.Recorder = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startRecording(stream) {
|
deviceReady() {
|
||||||
this.recorder = RecordRTC(stream, this.optionsRecorder);
|
this.player.record().start();
|
||||||
this.recorder.onStateChanged = this.onStateRecorderChanged;
|
},
|
||||||
this.recorder.startRecording();
|
startRecord() {
|
||||||
|
this.fireStateRecorderChanged('recording');
|
||||||
|
},
|
||||||
|
stopRecord() {
|
||||||
|
this.fireStateRecorderChanged('stopped');
|
||||||
|
},
|
||||||
|
finishRecord() {
|
||||||
|
const file = new File(
|
||||||
|
[this.player.recordedData],
|
||||||
|
this.player.recordedData.name,
|
||||||
|
{ type: this.player.recordedData.type }
|
||||||
|
);
|
||||||
|
this.fireRecorderBlob(file);
|
||||||
|
},
|
||||||
|
progressRecord() {
|
||||||
|
this.fireProgressRecord(this.formatTimeProgress());
|
||||||
},
|
},
|
||||||
stopAudioRecording() {
|
stopAudioRecording() {
|
||||||
if (this.isRecording) {
|
this.player.record().stop();
|
||||||
this.recorder.stopRecording(() => {
|
|
||||||
this.wavesurfer.microphone.stopDevice();
|
|
||||||
this.wavesurfer.loadBlob(this.recorder.getBlob());
|
|
||||||
this.wavesurfer.stop();
|
|
||||||
this.fireRecorderBlob(this.getAudioFile());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getAudioFile() {
|
deviceError() {
|
||||||
if (this.hasAudio()) {
|
const deviceError = this.player.deviceErrorCode;
|
||||||
return new File([this.recorder.getBlob()], this.getAudioFileName(), {
|
const deviceErrorName = deviceError?.name.toLowerCase();
|
||||||
type: 'audio/wav',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
hasAudio() {
|
|
||||||
return !(this.isRecording || this.wavesurfer.isPlaying());
|
|
||||||
},
|
|
||||||
playingRecorder() {
|
|
||||||
this.fireStateRecorderChanged('playing');
|
|
||||||
},
|
|
||||||
pausedRecorder() {
|
|
||||||
this.fireStateRecorderChanged('paused');
|
|
||||||
},
|
|
||||||
deviceError(err) {
|
|
||||||
if (
|
if (
|
||||||
err?.name &&
|
deviceErrorName?.includes('notallowederror') ||
|
||||||
(err.name.toLowerCase().includes('notallowederror') ||
|
deviceErrorName?.includes('permissiondeniederror')
|
||||||
err.name.toLowerCase().includes('permissiondeniederror'))
|
|
||||||
) {
|
) {
|
||||||
this.showAlert(
|
this.showAlert(
|
||||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
||||||
|
@ -127,56 +152,37 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateRecorderChanged(state) {
|
formatTimeProgress() {
|
||||||
// recording stopped inactive destroyed
|
return format(
|
||||||
switch (state) {
|
addSeconds(
|
||||||
case 'recording':
|
new Date(this.recordingDateStarted.getTimezoneOffset() * 1000 * 60),
|
||||||
this.timerDurationChanged();
|
this.player.record().getDuration()
|
||||||
break;
|
),
|
||||||
case 'stopped':
|
'mm:ss'
|
||||||
this.timerDurationChanged();
|
);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.fireStateRecorderChanged(state);
|
|
||||||
},
|
|
||||||
timerDurationChanged() {
|
|
||||||
if (this.isRecording) {
|
|
||||||
this.recordingInterval = setInterval(() => {
|
|
||||||
this.calculateTimeDuration(
|
|
||||||
(new Date().getTime() - this.recordingDateStarted) / 1000
|
|
||||||
);
|
|
||||||
this.fireStateRecorderTimerChanged(this.timeDuration);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
clearInterval(this.recordingInterval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculateTimeDuration(secs) {
|
|
||||||
let hr = Math.floor(secs / 3600);
|
|
||||||
let min = Math.floor((secs - hr * 3600) / 60);
|
|
||||||
let sec = Math.floor(secs - hr * 3600 - min * 60);
|
|
||||||
if (min < 10) {
|
|
||||||
min = '0' + min;
|
|
||||||
}
|
|
||||||
if (sec < 10) {
|
|
||||||
sec = '0' + sec;
|
|
||||||
}
|
|
||||||
if (hr <= 0) {
|
|
||||||
this.timeDuration = min + ':' + sec;
|
|
||||||
} else {
|
|
||||||
if (hr < 10) {
|
|
||||||
hr = '0' + hr;
|
|
||||||
}
|
|
||||||
this.timeDuration = hr + ':' + min + ':' + sec;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
playPause() {
|
playPause() {
|
||||||
this.wavesurfer.playPause();
|
if (this.player.wavesurfer().surfer.isPlaying()) {
|
||||||
|
this.fireStateRecorderChanged('paused');
|
||||||
|
} else {
|
||||||
|
this.fireStateRecorderChanged('playing');
|
||||||
|
}
|
||||||
|
this.player.wavesurfer().surfer.playPause();
|
||||||
|
},
|
||||||
|
play() {
|
||||||
|
this.fireStateRecorderChanged('playing');
|
||||||
|
this.player.wavesurfer().play();
|
||||||
|
},
|
||||||
|
pause() {
|
||||||
|
this.fireStateRecorderChanged('paused');
|
||||||
|
this.player.wavesurfer().pause();
|
||||||
|
},
|
||||||
|
playbackFinish() {
|
||||||
|
this.fireStateRecorderChanged('paused');
|
||||||
|
this.player.wavesurfer().pause();
|
||||||
},
|
},
|
||||||
fireRecorderBlob(blob) {
|
fireRecorderBlob(blob) {
|
||||||
this.$emit('recorder-blob', {
|
this.$emit('finish-record', {
|
||||||
name: blob.name,
|
name: blob.name,
|
||||||
type: blob.type,
|
type: blob.type,
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
|
@ -186,29 +192,8 @@ export default {
|
||||||
fireStateRecorderChanged(state) {
|
fireStateRecorderChanged(state) {
|
||||||
this.$emit('state-recorder-changed', state);
|
this.$emit('state-recorder-changed', state);
|
||||||
},
|
},
|
||||||
fireStateRecorderTimerChanged(duration) {
|
fireProgressRecord(duration) {
|
||||||
this.$emit('state-recorder-timer-changed', duration);
|
this.$emit('state-recorder-progress-changed', duration);
|
||||||
},
|
|
||||||
getAudioFileName() {
|
|
||||||
const d = new Date();
|
|
||||||
return `audio-${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${this.getRandomString()}.wav`;
|
|
||||||
},
|
|
||||||
getRandomString() {
|
|
||||||
if (
|
|
||||||
window.crypto &&
|
|
||||||
window.crypto.getRandomValues &&
|
|
||||||
navigator.userAgent.indexOf('Safari') === -1
|
|
||||||
) {
|
|
||||||
let a = window.crypto.getRandomValues(new Uint32Array(3));
|
|
||||||
let token = '';
|
|
||||||
for (let i = 0, l = a.length; i < l; i += 1) {
|
|
||||||
token += a[i].toString(36);
|
|
||||||
}
|
|
||||||
return token.toLowerCase();
|
|
||||||
}
|
|
||||||
return (Math.random() * new Date().getTime())
|
|
||||||
.toString(36)
|
|
||||||
.replace(/\./g, '');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -217,7 +202,9 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.audio-wave-wrapper {
|
.audio-wave-wrapper {
|
||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
max-height: 12rem;
|
height: 8rem;
|
||||||
overflow: hidden;
|
}
|
||||||
|
.video-js .vjs-control-bar {
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="insertCannedResponse"
|
||||||
/>
|
/>
|
||||||
<div ref="editor"></div>
|
<div ref="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,16 @@
|
||||||
:title="signatureToggleTooltip"
|
:title="signatureToggleTooltip"
|
||||||
@click="toggleMessageSignature"
|
@click="toggleMessageSignature"
|
||||||
/>
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="hasWhatsappTemplates"
|
||||||
|
v-tooltip.top-end="'Whatsapp Templates'"
|
||||||
|
icon="whatsapp"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="smooth"
|
||||||
|
size="small"
|
||||||
|
:title="'Whatsapp Templates'"
|
||||||
|
@click="$emit('selectWhatsappTemplate')"
|
||||||
|
/>
|
||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<div
|
||||||
v-show="$refs.upload && $refs.upload.dropActive"
|
v-show="$refs.upload && $refs.upload.dropActive"
|
||||||
|
@ -218,6 +228,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
hasWhatsappTemplates: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isNote() {
|
isNote() {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue