Compare commits
156
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd1c84119d | ||
|
|
c8fac08e0b | ||
|
|
7fd220c177 | ||
|
|
b3b76d00ff | ||
|
|
2f2ae88cba | ||
|
|
2b7989f52b | ||
|
|
d7314079c9 | ||
|
|
1017903ee1 | ||
|
|
d25e7fd54e | ||
|
|
7e89e3a34a | ||
|
|
39eaed7766 | ||
|
|
b1a4907ede | ||
|
|
a01d81a7e1 | ||
|
|
117d5301b4 | ||
|
|
aae6081d73 | ||
|
|
d03924b846 | ||
|
|
b333d0c986 | ||
|
|
9f3d155822 | ||
|
|
9203b097d2 | ||
|
|
12cb97db5f | ||
|
|
09971fd613 | ||
|
|
ca9d108741 | ||
|
|
f28533bc47 | ||
|
|
68f82c11a5 | ||
|
|
abc27fa791 | ||
|
|
d93a8d05bc | ||
|
|
373f5f5b64 | ||
|
|
ae7df60a75 | ||
|
|
412d750b6a | ||
|
|
f1a77ba934 | ||
|
|
ffc6364690 | ||
|
|
3a7633b564 | ||
|
|
91d1d44aa9 | ||
|
|
afc97faa8b | ||
|
|
1d0930ef63 | ||
|
|
776ea6d39a | ||
|
|
2c85098698 | ||
|
|
6bd0e074dc | ||
|
|
123fc73394 | ||
|
|
88cef88d80 | ||
|
|
8e79cf72e1 | ||
|
|
bfaca851f1 | ||
|
|
5618b5ebd3 | ||
|
|
4807052c37 | ||
|
|
44b1047b90 | ||
|
|
9c6c19c3e5 | ||
|
|
2764338453 | ||
|
|
4f0fcfcbcd | ||
|
|
fdbaf98c3b | ||
|
|
03bbd048a7 | ||
|
|
d481b9fbcf | ||
|
|
68dedc37ba | ||
|
|
958c0d7946 | ||
|
|
9553329092 | ||
|
|
d01f2063d0 | ||
|
|
590e4e9c1c | ||
|
|
0e903d2365 | ||
|
|
d6ce1ceeeb | ||
|
|
e8f56d0e56 | ||
|
|
b8dae07c0f | ||
|
|
e3f4be97c0 | ||
|
|
1f4d096804 | ||
|
|
1bcc438b21 | ||
|
|
8b3d13a920 | ||
|
|
dcdd58bae5 | ||
|
|
7b9a9831c1 | ||
|
|
7ab7bac6bf | ||
|
|
b988a01df3 | ||
|
|
105f9a27d2 | ||
|
|
132cf802d5 | ||
|
|
f2f33038f2 | ||
|
|
590ce788b9 | ||
|
|
a25179c342 | ||
|
|
aea9470b6a | ||
|
|
6382707146 | ||
|
|
49ef4e54ca | ||
|
|
db0d32f01d | ||
|
|
de1fb6ba4e | ||
|
|
7b8bcaba11 | ||
|
|
3f39b7ad1f | ||
|
|
a3547c5a1f | ||
|
|
0f776a173c | ||
|
|
d320605556 | ||
|
|
ca85d4e1c6 | ||
|
|
b994706265 | ||
|
|
d9f2cd0a6e | ||
|
|
f2a34ad8aa | ||
|
|
65e91f6a6f | ||
|
|
7dd1562b40 | ||
|
|
c70367bbd7 | ||
|
|
5a8eff35b5 | ||
|
|
1936369b12 | ||
|
|
354010a6e1 | ||
|
|
385eab6b96 | ||
|
|
198cd9b28d | ||
|
|
abdf00d2cf | ||
|
|
2c3160cfee | ||
|
|
708bddf4db | ||
|
|
020dcc4dc7 | ||
|
|
271263bcc2 | ||
|
|
c97d6021e0 | ||
|
|
d99997d17d | ||
|
|
9c5d062efc | ||
|
|
87f758ee1f | ||
|
|
d0a1ad746a | ||
|
|
86384bf876 | ||
|
|
55e5fa2205 | ||
|
|
65bad90b69 | ||
|
|
07aaa046c1 | ||
|
|
520bdabefe | ||
|
|
662967b5d3 | ||
|
|
6137a45214 | ||
|
|
5e0ce7793c | ||
|
|
ce3e38df0f | ||
|
|
2b88ecdfc4 | ||
|
|
683f259771 | ||
|
|
51fb3b7e8e | ||
|
|
3dedfee350 | ||
|
|
022383d942 | ||
|
|
59433d9d3c | ||
|
|
85e57c2e94 | ||
|
|
b081fe08b8 | ||
|
|
0d014d5d4b | ||
|
|
937338e3ea | ||
|
|
6e506b070e | ||
|
|
18c5c47836 | ||
|
|
257a294166 | ||
|
|
12f121f0d8 | ||
|
|
847d7ea082 | ||
|
|
c8041392dc | ||
|
|
be3c75e858 | ||
|
|
02c467b6db | ||
|
|
13fe439d9f | ||
|
|
2313edd244 | ||
|
|
09c9300c21 | ||
|
|
c0e905b5d8 | ||
|
|
04da8aa8dc | ||
|
|
32f7342cd6 | ||
|
|
74afb785c2 | ||
|
|
cae1e30160 | ||
|
|
99dfe1d5af | ||
|
|
3fa654f5c6 | ||
|
|
37fde64c67 | ||
|
|
3c2d6faf68 | ||
|
|
5d30dabf97 | ||
|
|
cef1f97d18 | ||
|
|
92705723f1 | ||
|
|
af971c9b18 | ||
|
|
2f2cdd7e7c | ||
|
|
d95283f5c2 | ||
|
|
402428fb4d | ||
|
|
5600b518ac | ||
|
|
7bd400772d | ||
|
|
3e6d23e071 | ||
|
|
0e3eb51d7a | ||
|
|
4ffe73a1ce |
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.1.3-browsers
|
||||
- image: cimg/ruby:3.2.2-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- persist_to_workspace:
|
||||
root: coverage
|
||||
|
||||
@@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
npm
|
||||
|
||||
# Install rbenv and ruby
|
||||
ARG RUBY_VERSION="3.1.3"
|
||||
ARG RUBY_VERSION="3.2.2"
|
||||
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||
|
||||
@@ -173,6 +173,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
## LogRocket
|
||||
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
|
||||
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See the error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
Share a clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Browser logs**
|
||||
|
||||
Share the browser logs to debug the issue further.
|
||||
|
||||
**Server logs**
|
||||
|
||||
Share the server logs to debug the issue further.
|
||||
|
||||
**Environment**
|
||||
|
||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||
|
||||
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
||||
- [ ] Self-hosted
|
||||
- - [ ] Linux VM
|
||||
- - [ ] Docker
|
||||
- - [ ] Kubernetes
|
||||
- - [ ] Heroku
|
||||
- - [ ] Other (Please specify)
|
||||
|
||||
|
||||
**Desktop (please complete the following information)** (If applicable)
|
||||
- OS: [e.g. Linux, Windows, MacOS]
|
||||
- Browser [e.g. chrome, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information)** (If applicable)
|
||||
- Device: [e.g. iPhone6, Pixel7]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Docker** (If applicable)
|
||||
|
||||
Please share the output of the following.
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker-compose version`
|
||||
|
||||
**Cloud Provider** (If applicable)
|
||||
- [ ] AWS
|
||||
- [ ] GCP
|
||||
- [ ] Azure
|
||||
- [ ] DigitalOcean
|
||||
- [ ] Others
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,78 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a report to help us improve
|
||||
labels: 'Bug'
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A concise description of what you expected to happen along with screenshots if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||
options:
|
||||
- app.chatwoot.com
|
||||
- Linux VM
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- Heroku
|
||||
- Other [please specify in the description]
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: provider
|
||||
attributes:
|
||||
label: Cloud Provider
|
||||
description:
|
||||
options:
|
||||
- AWS
|
||||
- GCP
|
||||
- Azure
|
||||
- DigitalOcean
|
||||
- Other [please specify in the description]
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Describe the platform you are using
|
||||
options:
|
||||
- Browser
|
||||
- Mobile
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: The operating system and the version you are using.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Browser and version
|
||||
description: The name of the browser and version you are using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Docker (if applicable)
|
||||
description: |
|
||||
Please share the output of the following.
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker-compose version`
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a security issue
|
||||
url: https://www.chatwoot.com/docs/contributing-guide/security-reports/
|
||||
about: Guidelines and steps to report a security vulnerability. Please report security vulnerabilities here.
|
||||
- name: Product Documentation
|
||||
url: https://www.chatwoot.com/help-center
|
||||
about: If you have questions, are confused, or just want to understand our product better, please check out our documentation.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest any enhancements for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your enhancement request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the enhancement request here.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,28 @@
|
||||
name: 🧙 Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: 'feature-request'
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature or enhancement related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Run Size Limit Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
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:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
|
||||
- name: Size Check
|
||||
run: yarn run size
|
||||
@@ -67,3 +67,10 @@ test/cypress/videos/*
|
||||
# yalc for local testing
|
||||
.yalc
|
||||
yalc.lock
|
||||
|
||||
/public/packs
|
||||
/public/packs-test
|
||||
/node_modules
|
||||
/yarn-error.log
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
|
||||
@@ -162,8 +162,33 @@ Rails/RenderInline:
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
|
||||
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
|
||||
- 'db/migrate/20200606132552_create_labels.rb'
|
||||
- 'db/migrate/20201027135006_create_working_hours.rb'
|
||||
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
|
||||
- 'db/migrate/20210114202310_create_teams.rb'
|
||||
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
|
||||
- 'db/migrate/20210428135041_add_campaigns.rb'
|
||||
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
|
||||
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
|
||||
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
|
||||
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
|
||||
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
|
||||
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
|
||||
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
|
||||
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
|
||||
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
|
||||
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
|
||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.1.3
|
||||
3.2.2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/spec
|
||||
@@ -1,10 +1,10 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.1.3'
|
||||
ruby '3.2.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rails', '~> 6.1', '>= 6.1.7.3'
|
||||
gem 'rails', '~> 7'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
@@ -36,19 +36,19 @@ gem 'json_schemer'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down', '~> 5.0'
|
||||
gem 'down'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
gem 'gmail_xoauth'
|
||||
# Prevent CSV injection
|
||||
gem 'csv-safe'
|
||||
# Support message translation
|
||||
gem 'google-cloud-translate'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
# original gem isn't maintained actively
|
||||
# we wanted updated version of faraday which is a dependency for slack-ruby-client
|
||||
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'image_processing', '~> 1.12.2'
|
||||
gem 'image_processing'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
@@ -62,13 +62,13 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'webpacker', '~> 5.4', '>= 5.4.3'
|
||||
gem 'webpacker'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
|
||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
@@ -81,7 +81,6 @@ gem 'administrate'
|
||||
gem 'wisper', '2.0.0'
|
||||
|
||||
##--- gems for channels ---##
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.66'
|
||||
@@ -91,9 +90,14 @@ gem 'twitty', '~> 0.1.5'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client'
|
||||
gem 'slack-ruby-client', '~> 2.0.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow'
|
||||
gem 'google-cloud-dialogflow-v2'
|
||||
gem 'grpc'
|
||||
# Translate integrations
|
||||
# 'google-cloud-translate' gem depends on faraday 2.0 version
|
||||
# this dependency breaks the slack-ruby-client gem
|
||||
gem 'google-cloud-translate-v3'
|
||||
|
||||
##-- apm and error monitoring ---#
|
||||
# loaded only when environment variables are set.
|
||||
@@ -108,9 +112,9 @@ gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '~> 6.4.2'
|
||||
gem 'sidekiq'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '~> 1.6', '>= 1.6.0'
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
@@ -129,7 +133,10 @@ gem 'procore-sift'
|
||||
|
||||
# parse email
|
||||
gem 'email_reply_trimmer'
|
||||
gem 'html2text'
|
||||
|
||||
# TODO: we might have to fork this gem since 0.3.1 has hard depency on nokogir 1.10.
|
||||
# and this gem hasn't been updated for a while.
|
||||
gem 'html2text', git: 'https://github.com/chatwoot/html2text_ruby', branch: 'chatwoot'
|
||||
|
||||
# to calculate working hours
|
||||
gem 'working_hours'
|
||||
@@ -144,18 +151,13 @@ gem 'stripe'
|
||||
## to populate db with sample data
|
||||
gem 'faker'
|
||||
|
||||
# Can remove this in rails 7
|
||||
gem 'net-imap', require: false
|
||||
gem 'net-pop', require: false
|
||||
gem 'net-smtp', require: false
|
||||
|
||||
# Include logrange conditionally in intializer using env variable
|
||||
gem 'lograge', '~> 0.12.0', require: false
|
||||
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'audited', '~> 5.2'
|
||||
gem 'audited', '~> 5.3'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth'
|
||||
@@ -174,6 +176,7 @@ group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
gem 'letter_opener'
|
||||
gem 'scss_lint', require: false
|
||||
gem 'web-console'
|
||||
|
||||
# used in swagger build
|
||||
@@ -189,7 +192,7 @@ end
|
||||
|
||||
group :test do
|
||||
# Cypress in rails.
|
||||
gem 'cypress-on-rails', '~> 1.13', '>= 1.13.1'
|
||||
gem 'cypress-on-rails'
|
||||
# fast cleaning of database
|
||||
gem 'database_cleaner'
|
||||
# mock http calls
|
||||
@@ -211,7 +214,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '~> 5.0.3'
|
||||
gem 'rspec-rails'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+341
-323
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1 +1 @@
|
||||
2.2.0
|
||||
2.17.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.1.0
|
||||
2.3.0
|
||||
|
||||
@@ -63,28 +63,43 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
@conversation ||= Conversation.where(conversation_params).find_by(
|
||||
"additional_attributes ->> 'type' = 'instagram_direct_message'"
|
||||
) || build_conversation
|
||||
end
|
||||
|
||||
def message_content
|
||||
@messaging[:message][:text]
|
||||
end
|
||||
|
||||
def story_reply_attributes
|
||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
|
||||
@message = conversation.messages.create!(message_params)
|
||||
save_story_id
|
||||
|
||||
attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def save_story_id
|
||||
return if story_reply_attributes.blank?
|
||||
|
||||
@message.save_story_info(story_reply_attributes)
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
||||
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: { type: 'instagram_direct_message' }
|
||||
))
|
||||
end
|
||||
|
||||
@@ -92,10 +107,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id,
|
||||
additional_attributes: {
|
||||
type: 'instagram_direct_message'
|
||||
}
|
||||
contact_id: contact.id
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -96,10 +96,9 @@ class V2::ReportBuilder
|
||||
|
||||
def conversations
|
||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
unattended: @open_conversations.unattended.count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
metric
|
||||
|
||||
@@ -16,6 +16,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@article = @portal.articles.create!(article_params)
|
||||
@article.associate_root_article(article_params[:associated_article_id])
|
||||
@@ -23,10 +26,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@article.update!(article_params) if params[:article].present?
|
||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||
|
||||
@@ -6,6 +6,8 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
@automation_rules = Current.account.automation_rules
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions]
|
||||
@@ -28,8 +30,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
|
||||
@@ -6,21 +6,21 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
@campaigns = Current.account.campaigns
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@campaign = Current.account.campaigns.create!(campaign_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@campaign.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def campaign
|
||||
|
||||
@@ -9,6 +9,8 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
@categories = @portal.categories.search(params)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@category = @portal.categories.create!(category_params)
|
||||
@category.related_categories << related_categories_records
|
||||
@@ -17,8 +19,6 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
@category.save!
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@category.update!(category_params)
|
||||
@category.related_categories = related_categories_records if related_categories_records.any?
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
||||
include HmacConcern
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
|
||||
def create
|
||||
@contact_inbox = ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id]
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
).perform
|
||||
end
|
||||
|
||||
|
||||
@@ -5,21 +5,21 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
||||
@notes = @contact.notes.latest.includes(:user)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@note = @contact.notes.create!(note_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@note.update(note_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@note.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@note.update(note_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
include HmacConcern
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||
@@ -22,6 +23,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def attachments
|
||||
@attachments = @conversation.attachments
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||
@@ -29,8 +36,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def filter
|
||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
||||
@conversations = result[:conversations]
|
||||
@@ -100,9 +105,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def set_conversation_status
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
@@ -148,7 +150,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id]
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
).perform
|
||||
end
|
||||
|
||||
|
||||
@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
||||
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
|
||||
.filter_by_assigned_agent_id(params[:user_ids])
|
||||
.filter_by_inbox_id(params[:inbox_id])
|
||||
.filter_by_team_id(params[:team_id])
|
||||
.filter_by_rating(params[:rating])
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
|
||||
@@ -11,6 +11,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
|
||||
@custom_filter = current_user.custom_filters.create!(
|
||||
permitted_payload.merge(account_id: Current.account.id)
|
||||
)
|
||||
render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid?
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -2,6 +2,11 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
before_action :fetch_inbox
|
||||
before_action :current_agents_ids, only: [:create, :update]
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
ActiveRecord::Base.transaction do
|
||||
@@ -10,11 +15,6 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @inbox, :update?
|
||||
update_agents_list
|
||||
|
||||
@@ -44,26 +44,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def update
|
||||
@inbox.update!(permitted_params.except(:channel))
|
||||
update_inbox_working_hours
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
|
||||
# Inbox update doesn't necessarily need channel attributes
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
if @inbox.inbox_type == 'Email'
|
||||
begin
|
||||
validate_email_channel(channel_attributes)
|
||||
rescue StandardError => e
|
||||
render json: { message: e }, status: :unprocessable_entity and return
|
||||
end
|
||||
@inbox.channel.reauthorized!
|
||||
end
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
def update_inbox_working_hours
|
||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||
update_channel if channel_update_required?
|
||||
end
|
||||
|
||||
def agent_bot
|
||||
@@ -103,6 +84,35 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
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 update_channel
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
validate_and_update_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||
|
||||
reauthorize_and_update_channel(channel_attributes)
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
def channel_update_required?
|
||||
permitted_params(get_channel_attributes(@inbox.channel_type))[:channel].present?
|
||||
end
|
||||
|
||||
def validate_and_update_email_channel(channel_attributes)
|
||||
validate_email_channel(channel_attributes)
|
||||
rescue StandardError => e
|
||||
render json: { message: e }, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
def reauthorize_and_update_channel(channel_attributes)
|
||||
@inbox.channel.reauthorized! if @inbox.channel.respond_to?(:reauthorized!)
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless @inbox.web_widget?
|
||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||
|
||||
@@ -6,6 +6,10 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
@macros = Macro.with_visibility(current_user, params)
|
||||
end
|
||||
|
||||
def show
|
||||
head :not_found if @macro.nil?
|
||||
end
|
||||
|
||||
def create
|
||||
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
@@ -18,8 +22,16 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
@macro
|
||||
end
|
||||
|
||||
def show
|
||||
head :not_found if @macro.nil?
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -37,18 +49,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def execute
|
||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
ActiveRecord::Base.transaction do
|
||||
@portal.update!(portal_params) if params[:portal].present?
|
||||
# @portal.custom_domain = parsed_custom_domain
|
||||
process_attached_logo
|
||||
process_attached_logo if params[:blob_id].present?
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
|
||||
|
||||
@@ -14,6 +14,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
|
||||
def show
|
||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||
render 'api/v1/accounts/show', format: :json
|
||||
end
|
||||
|
||||
def create
|
||||
@user, @account = AccountBuilder.new(
|
||||
account_name: account_params[:account_name],
|
||||
@@ -35,11 +40,6 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
end
|
||||
|
||||
def show
|
||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||
render 'api/v1/accounts/show', format: :json
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
end
|
||||
|
||||
@@ -48,7 +48,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
def message_finder_params
|
||||
{
|
||||
filter_internal_messages: true,
|
||||
before: permitted_params[:before]
|
||||
before: permitted_params[:before],
|
||||
after: permitted_params[:after]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -62,7 +63,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@@ -3,7 +3,7 @@ class ApiController < ApplicationController
|
||||
|
||||
def index
|
||||
render json: { version: Chatwoot.config[:version],
|
||||
timestamp: Time.now.utc.to_formatted_s(:db),
|
||||
timestamp: Time.now.utc.to_fs(:db),
|
||||
queue_services: redis_status,
|
||||
data_services: postgres_status }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module HmacConcern
|
||||
def hmac_verified?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:hmac_verified]).present?
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,16 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :require_no_authentication, raise: false
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:email])
|
||||
if @user
|
||||
@user.send_reset_password_instructions
|
||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||
else
|
||||
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
# params: reset_password_token, password, password_confirmation
|
||||
original_token = params[:reset_password_token]
|
||||
@@ -17,16 +27,6 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:email])
|
||||
if @user
|
||||
@user.send_reset_password_instructions
|
||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||
else
|
||||
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_password_and_confirmation(recoverable)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsController
|
||||
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
# Prevent session parameter from being passed
|
||||
# Unpermitted parameter: session
|
||||
wrap_parameters format: []
|
||||
@@ -37,3 +37,5 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
||||
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||
end
|
||||
end
|
||||
|
||||
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenValidationsController
|
||||
class DeviseOverrides::TokenValidationsController < DeviseTokenAuth::TokenValidationsController
|
||||
def validate_token
|
||||
# @resource will have been set by set_user_by_token concern
|
||||
if @resource
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
class Platform::Api::V1::AccountsController < PlatformController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = Account.create!(account_params)
|
||||
update_resource_features
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(account_params)
|
||||
update_resource_features
|
||||
|
||||
@@ -6,14 +6,14 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = AgentBot.new(agent_bot_params)
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(agent_bot_params)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
before_action(only: [:login]) { set_resource }
|
||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.skip_confirmation!
|
||||
@@ -16,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
render json: { url: @resource.generate_sso_link }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(user_update_params)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
before_action :contact_inbox, except: [:create]
|
||||
before_action :process_hmac
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
@@ -11,8 +13,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
).perform
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
contact_identify_action = ContactIdentifyAction.new(
|
||||
contact: @contact_inbox.contact,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :set_category, except: [:index]
|
||||
before_action :set_category, except: [:index, :show]
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
@@ -16,7 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
private
|
||||
|
||||
def set_article
|
||||
@article = @category.articles.find(permitted_params[:id])
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@article.increment_view_count
|
||||
@parsed_content = render_article_content(@article.content)
|
||||
end
|
||||
@@ -39,7 +39,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:slug, :category_slug, :locale, :id)
|
||||
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
||||
end
|
||||
|
||||
def render_article_content(content)
|
||||
|
||||
@@ -5,6 +5,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
|
||||
yield
|
||||
end
|
||||
|
||||
def switch_locale_with_portal(&)
|
||||
@@ -19,4 +22,16 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
else
|
||||
'en'
|
||||
end
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,13 +45,24 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
|
||||
def seed
|
||||
Internal::SeedAccountJob.perform_later(requested_resource)
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
def reset_cache
|
||||
requested_resource.reset_cache_keys
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Cache keys cleared')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
def destroy
|
||||
account = Account.find(params[:id])
|
||||
|
||||
DeleteObjectJob.perform_later(account) if account.present?
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,17 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
sha
|
||||
postgres_status
|
||||
redis_metrics
|
||||
chatwoot_edition
|
||||
end
|
||||
|
||||
def chatwoot_edition
|
||||
@metrics['Chatwoot edition'] = if ChatwootApp.enterprise?
|
||||
'Enterprise'
|
||||
elsif ChatwootApp.custom?
|
||||
'Custom'
|
||||
else
|
||||
'Community'
|
||||
end
|
||||
end
|
||||
|
||||
def chatwoot_version
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class SwaggerController < ApplicationController
|
||||
def respond
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||
render inline: Rails.root.join('swagger', derived_path).read
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ class WidgetsController < ActionController::Base
|
||||
source_id: @auth_token_params[:source_id]
|
||||
)
|
||||
|
||||
@contact = @contact_inbox ? @contact_inbox.contact : nil
|
||||
@contact = @contact_inbox&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
class AccountDrop < BaseDrop
|
||||
def name
|
||||
@obj.try(:name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,8 @@ class ConversationFinder
|
||||
SORT_OPTIONS = {
|
||||
latest: 'latest',
|
||||
sort_on_created_at: 'sort_on_created_at',
|
||||
last_user_message_at: 'last_user_message_at'
|
||||
last_user_message_at: 'last_user_message_at',
|
||||
sort_on_priority: 'sort_on_priority'
|
||||
}.with_indifferent_access
|
||||
|
||||
# assumptions
|
||||
@@ -53,9 +54,10 @@ class ConversationFinder
|
||||
|
||||
find_all_conversations
|
||||
filter_by_status unless params[:q]
|
||||
filter_by_team if @team
|
||||
filter_by_labels if params[:labels]
|
||||
filter_by_query if params[:q]
|
||||
filter_by_team
|
||||
filter_by_labels
|
||||
filter_by_query
|
||||
filter_by_source_id
|
||||
end
|
||||
|
||||
def set_inboxes
|
||||
@@ -100,12 +102,14 @@ class ConversationFinder
|
||||
when 'participating'
|
||||
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
|
||||
when 'unattended'
|
||||
@conversations = @conversations.where(first_reply_created_at: nil)
|
||||
@conversations = @conversations.unattended
|
||||
end
|
||||
@conversations
|
||||
end
|
||||
|
||||
def filter_by_query
|
||||
return unless params[:q]
|
||||
|
||||
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
||||
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
||||
.where(messages: { message_type: allowed_message_types }).includes(:messages)
|
||||
@@ -120,13 +124,24 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def filter_by_team
|
||||
return unless @team
|
||||
|
||||
@conversations = @conversations.where(team: @team)
|
||||
end
|
||||
|
||||
def filter_by_labels
|
||||
return unless params[:labels]
|
||||
|
||||
@conversations = @conversations.tagged_with(params[:labels], any: true)
|
||||
end
|
||||
|
||||
def filter_by_source_id
|
||||
return unless params[:source_id]
|
||||
|
||||
@conversations = @conversations.joins(:contact_inbox)
|
||||
@conversations = @conversations.where(contact_inboxes: { source_id: params[:source_id] })
|
||||
end
|
||||
|
||||
def set_count_for_all_conversations
|
||||
[
|
||||
@conversations.assigned_to(current_user).count,
|
||||
@@ -140,9 +155,9 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def conversations
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||
)
|
||||
@conversations = @conversations.includes(:taggings, :inbox,
|
||||
{ assignee: [{ account_users: [:account] }, { avatar_attachment: [:blob] }] },
|
||||
{ contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox, :messages)
|
||||
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
|
||||
@conversations.send(sort_by).page(current_page)
|
||||
end
|
||||
|
||||
@@ -10,4 +10,26 @@ module EmailHelper
|
||||
def normalize_email_with_plus_addressing(email)
|
||||
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
|
||||
end
|
||||
|
||||
def parse_email_variables(conversation, email)
|
||||
case email
|
||||
when modified_liquid_content(email)
|
||||
template = Liquid::Template.parse(modified_liquid_content(email))
|
||||
template.render(message_drops(conversation))
|
||||
when URI::MailTo::EMAIL_REGEXP
|
||||
email
|
||||
end
|
||||
end
|
||||
|
||||
def modified_liquid_content(email)
|
||||
# This regex is used to match the code blocks in the content
|
||||
# We don't want to process liquid in code blocks
|
||||
email.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
|
||||
end
|
||||
|
||||
def message_drops(conversation)
|
||||
{
|
||||
'contact' => ContactDrop.new(conversation.contact)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
|
||||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to, user_ids } = {}) {
|
||||
get({ page, from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
inbox_id,
|
||||
team_id,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
download({ from, to, user_ids } = {}) {
|
||||
download({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||
return axios.get(`${this.url}/download`, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
inbox_id,
|
||||
team_id,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to, user_ids } = {}) {
|
||||
getMetrics({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||
// no ratings for metrics
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to, user_ids },
|
||||
params: { since: from, until: to, user_ids, inbox_id, team_id, rating },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class ConversationApi extends ApiClient {
|
||||
labels,
|
||||
teamId,
|
||||
conversationType,
|
||||
sortBy,
|
||||
}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
@@ -24,6 +25,7 @@ class ConversationApi extends ApiClient {
|
||||
page,
|
||||
labels,
|
||||
conversation_type: conversationType,
|
||||
sort_by: sortBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -125,6 +127,10 @@ class ConversationApi extends ApiClient {
|
||||
user_ids: userIds,
|
||||
});
|
||||
}
|
||||
|
||||
getAllAttachments(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
||||
@@ -7,14 +7,22 @@ class OpenAIAPI extends ApiClient {
|
||||
super('integrations', { accountScoped: true });
|
||||
}
|
||||
|
||||
processEvent({ name = 'rephrase', content, tone, hookId }) {
|
||||
processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) {
|
||||
let data = {
|
||||
tone,
|
||||
content,
|
||||
};
|
||||
|
||||
if (type === 'reply_suggestion' || type === 'summarize') {
|
||||
data = {
|
||||
conversation_display_id: conversationId,
|
||||
};
|
||||
}
|
||||
|
||||
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
|
||||
event: {
|
||||
name: name,
|
||||
data: {
|
||||
tone,
|
||||
content,
|
||||
},
|
||||
name: type,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,30 @@ class SearchAPI extends ApiClient {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
contacts({ q }) {
|
||||
return axios.get(`${this.url}/contacts`, {
|
||||
params: {
|
||||
q,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
conversations({ q }) {
|
||||
return axios.get(`${this.url}/conversations`, {
|
||||
params: {
|
||||
q,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
messages({ q }) {
|
||||
return axios.get(`${this.url}/messages`, {
|
||||
params: {
|
||||
q,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SearchAPI();
|
||||
|
||||
@@ -210,5 +210,12 @@ describe('#ConversationAPI', () => {
|
||||
{ params: { page: payload.page } }
|
||||
);
|
||||
});
|
||||
|
||||
it('#getAllAttachments', () => {
|
||||
conversationAPI.getAllAttachments(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/attachments'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
&:not(.auto-width) {
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
|
||||
@@ -254,7 +254,7 @@ $button-sizes: (tiny: var(--font-size-micro),
|
||||
default: var(--font-size-small),
|
||||
large: var(--font-size-medium));
|
||||
$button-palette: $foundation-palette;
|
||||
$button-opacity-disabled: 0.25;
|
||||
$button-opacity-disabled: 0.4;
|
||||
$button-background-hover-lightness: -20%;
|
||||
$button-hollow-hover-lightness: -50%;
|
||||
$button-transition: background-color 0.25s ease-out,
|
||||
|
||||
@@ -261,6 +261,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Basic filter dropdown
|
||||
.basic-filter {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
// Card label
|
||||
.label-container {
|
||||
.label {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-normal);
|
||||
&:not(.no-margin) {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
opacity: 0.8;
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-height: 90%;
|
||||
max-width: 90%;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
.modal-video {
|
||||
max-height: 75vh;
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@@ -53,16 +53,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
.modal-container {
|
||||
width: auto;
|
||||
|
||||
.modal--close {
|
||||
z-index: var(--z-index-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-list-wrap {
|
||||
@@ -95,10 +85,6 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--space-normal);
|
||||
|
||||
.page-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-box {
|
||||
@@ -404,4 +390,3 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,23 @@
|
||||
class="chat-list__top"
|
||||
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
||||
>
|
||||
<h1 class="page-title text-truncate" :title="pageTitle">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
|
||||
<div class="filter--actions">
|
||||
<chat-filter
|
||||
<div class="flex-center chat-list__title">
|
||||
<h1
|
||||
class="page-sub-title text-truncate margin-bottom-0"
|
||||
:title="pageTitle"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<span
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@statusFilterChange="updateStatusType"
|
||||
/>
|
||||
class="conversation--status-pill"
|
||||
>
|
||||
{{
|
||||
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="filter--actions">
|
||||
<div v-if="hasAppliedFilters && !hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
|
||||
@@ -39,6 +47,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
@@ -48,7 +64,6 @@
|
||||
@click="onClickOpenDeleteFoldersModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
|
||||
@@ -58,6 +73,10 @@
|
||||
size="tiny"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
/>
|
||||
<conversation-basic-filter
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@changeFilter="onBasicFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,8 +176,11 @@
|
||||
v-if="showAdvancedFilters"
|
||||
:initial-filter-types="advancedFilterTypes"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-folder-name="activeFolderName"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:is-folder-view="hasActiveFolders"
|
||||
@applyFilter="onApplyFilter"
|
||||
@updateFolder="onUpdateSavedFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
@@ -167,8 +189,8 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ChatFilter from './widgets/conversation/ChatFilter';
|
||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
|
||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||
import timeMixin from '../mixins/time';
|
||||
@@ -182,6 +204,9 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
@@ -199,10 +224,10 @@ export default {
|
||||
AddCustomViews,
|
||||
ChatTypeTabs,
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
ConversationAdvancedFilter,
|
||||
DeleteCustomViews,
|
||||
ConversationBulkActions,
|
||||
ConversationBasicFilter,
|
||||
},
|
||||
mixins: [
|
||||
timeMixin,
|
||||
@@ -245,6 +270,7 @@ export default {
|
||||
return {
|
||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||
activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
|
||||
showAdvancedFilters: false,
|
||||
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
||||
...filter,
|
||||
@@ -277,6 +303,11 @@ export default {
|
||||
appliedFilters: 'getAppliedConversationFilters',
|
||||
folders: 'customViews/getCustomViews',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
agentList: 'agents/getAgents',
|
||||
teamsList: 'teams/getTeams',
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
labels: 'labels/getLabels',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length !== 0;
|
||||
@@ -364,6 +395,7 @@ export default {
|
||||
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
|
||||
assigneeType: this.activeAssigneeTab,
|
||||
status: this.activeStatus,
|
||||
sortBy: this.activeSortBy,
|
||||
page: this.conversationListPagination,
|
||||
labels: this.label ? [this.label] : undefined,
|
||||
teamId: this.teamId || undefined,
|
||||
@@ -438,6 +470,9 @@ export default {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeFolderName() {
|
||||
return this.activeFolder?.name;
|
||||
},
|
||||
activeTeam() {
|
||||
if (this.teamId) {
|
||||
return this.$store.getters['teams/getTeam'](this.teamId);
|
||||
@@ -470,18 +505,21 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
activeFolder() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
chatLists() {
|
||||
this.chatsOnView = this.conversationList;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
this.resetAndFetchData();
|
||||
|
||||
if (this.hasActiveFolders) {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
}
|
||||
|
||||
bus.$on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
@@ -494,6 +532,15 @@ export default {
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchFilteredConversations(payload);
|
||||
},
|
||||
onUpdateSavedFilter(payload, folderName) {
|
||||
const payloadData = {
|
||||
...this.activeFolder,
|
||||
name: folderName,
|
||||
query: filterQueryGenerator(payload),
|
||||
};
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
@@ -507,15 +554,70 @@ export default {
|
||||
this.showDeleteFoldersModal = false;
|
||||
},
|
||||
onToggleAdvanceFiltersModal() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
if (!this.hasAppliedFilters && !this.hasActiveFolders) {
|
||||
this.initializeExistingFilterToModal();
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
this.initializeFolderToFilterModal(this.activeFolder);
|
||||
}
|
||||
this.showAdvancedFilters = true;
|
||||
},
|
||||
closeAdvanceFiltersModal() {
|
||||
this.showAdvancedFilters = false;
|
||||
this.appliedFilter = [];
|
||||
},
|
||||
setParamsForEditFolderModal() {
|
||||
// Here we are setting the params for edit folder modal to show the existing values.
|
||||
|
||||
// For agent, team, inboxes,and campaigns we get only the id's from the query.
|
||||
// So we are mapping the id's to the actual values.
|
||||
|
||||
// For labels we get the name of the label from the query.
|
||||
// If we delete the label from the label list then we will not be able to show the label name.
|
||||
|
||||
// For custom attributes we get only attribute key.
|
||||
// So we are mapping it to find the input type of the attribute to show in the edit folder modal.
|
||||
const params = {
|
||||
agents: this.agentList,
|
||||
teams: this.teamsList,
|
||||
inboxes: this.inboxesList,
|
||||
labels: this.labels,
|
||||
campaigns: this.campaigns,
|
||||
languages: languages,
|
||||
countries: countries,
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
]('conversation_attribute'),
|
||||
};
|
||||
return params;
|
||||
},
|
||||
initializeFolderToFilterModal(activeFolder) {
|
||||
// Here we are setting the params for edit folder modal.
|
||||
// To show the existing values. when we click on edit folder button.
|
||||
|
||||
// Here we get the query from the active folder.
|
||||
// And we are mapping the query to the actual values.
|
||||
// To show in the edit folder modal by the help of generateValuesForEditCustomViews helper.
|
||||
const query = activeFolder?.query?.payload;
|
||||
if (!Array.isArray(query)) return;
|
||||
|
||||
this.appliedFilter.push(
|
||||
...query.map(filter => ({
|
||||
attribute_key: filter.attribute_key,
|
||||
attribute_model: filter.attribute_model,
|
||||
filter_operator: filter.filter_operator,
|
||||
values: Array.isArray(filter.values)
|
||||
? generateValuesForEditCustomViews(
|
||||
filter,
|
||||
this.setParamsForEditFolderModal()
|
||||
)
|
||||
: [],
|
||||
query_operator: filter.query_operator,
|
||||
custom_attribute_type: filter.custom_attribute_type,
|
||||
}))
|
||||
);
|
||||
},
|
||||
getKeyboardListenerParams() {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
@@ -561,6 +663,7 @@ export default {
|
||||
}
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.appliedFilter = [];
|
||||
this.resetBulkActions();
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
@@ -573,7 +676,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.fetchConversations();
|
||||
this.appliedFilter = [];
|
||||
},
|
||||
fetchConversations() {
|
||||
this.$store
|
||||
@@ -625,11 +727,13 @@ export default {
|
||||
this.selectedConversations = [];
|
||||
this.selectedInboxes = [];
|
||||
},
|
||||
updateStatusType(index) {
|
||||
if (this.activeStatus !== index) {
|
||||
this.activeStatus = index;
|
||||
this.resetAndFetchData();
|
||||
onBasicFilterChange(value, type) {
|
||||
if (type === 'status') {
|
||||
this.activeStatus = value;
|
||||
} else {
|
||||
this.activeSortBy = value;
|
||||
}
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
openLastSavedItemInFolder() {
|
||||
const lastItemOfFolder = this.folders[this.folders.length - 1];
|
||||
@@ -878,11 +982,15 @@ export default {
|
||||
&.list--full-width {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.page-sub-title {
|
||||
font-size: var(--font-size-two);
|
||||
}
|
||||
}
|
||||
.filter--actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-micro);
|
||||
gap: var(--space-smaller);
|
||||
}
|
||||
|
||||
.filter__applied {
|
||||
@@ -899,4 +1007,19 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--status-pill {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-medium-gray);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin: var(--space-micro) var(--space-small) 0;
|
||||
padding: var(--space-smaller);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.chat-list__title {
|
||||
max-width: 85%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="sub-head">
|
||||
{{ subTitle }}
|
||||
<slot name="subTitle">
|
||||
{{ subTitle }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="note">
|
||||
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||
|
||||
@@ -157,6 +157,7 @@ const settings = accountId => ({
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
beta: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
:disabled="isDisabled || isLoading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<spinner v-if="isLoading" size="small" />
|
||||
<spinner
|
||||
v-if="isLoading"
|
||||
size="small"
|
||||
:color-scheme="showDarkSpinner ? 'dark' : ''"
|
||||
/>
|
||||
<emoji-or-icon
|
||||
v-else-if="icon || emoji"
|
||||
class="icon"
|
||||
@@ -108,6 +112,14 @@ export default {
|
||||
return 16;
|
||||
}
|
||||
},
|
||||
showDarkSpinner() {
|
||||
return (
|
||||
this.colorScheme === 'secondary' ||
|
||||
this.variant === 'clear' ||
|
||||
this.variant === 'link' ||
|
||||
this.variant === 'hollow'
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick(evt) {
|
||||
|
||||
@@ -1,45 +1,70 @@
|
||||
<template>
|
||||
<div v-if="isAIIntegrationEnabled" class="position-relative">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
|
||||
icon="wand"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleDropdown"
|
||||
/>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open ai-modal"
|
||||
>
|
||||
<h4 class="sub-block-title margin-top-1">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
|
||||
</p>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
|
||||
</label>
|
||||
<div class="tone__item">
|
||||
<select v-model="activeTone" class="status--filter small">
|
||||
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
|
||||
{{ tone.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer flex-container align-right">
|
||||
<woot-button variant="clear" size="small" @click="closeDropdown">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-loading="isGenerating"
|
||||
size="small"
|
||||
@click="processText"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</woot-button>
|
||||
<div v-if="!message">
|
||||
<woot-button
|
||||
v-if="isPrivateNote"
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE')"
|
||||
icon="book-pulse"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:is-loading="uiFlags.summarize"
|
||||
@click="processEvent('summarize')"
|
||||
/>
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_TITLE')"
|
||||
icon="wand"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:is-loading="uiFlags.reply_suggestion"
|
||||
@click="processEvent('reply_suggestion')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
|
||||
icon="text-grammar-wand"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleDropdown"
|
||||
/>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open ai-modal"
|
||||
>
|
||||
<h4 class="sub-block-title margin-top-1">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
|
||||
</p>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
|
||||
</label>
|
||||
<div class="tone__item">
|
||||
<select v-model="activeTone" class="status--filter small">
|
||||
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
|
||||
{{ tone.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer flex-container align-right">
|
||||
<woot-button variant="clear" size="small" @click="closeDropdown">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-loading="uiFlags.rephrase"
|
||||
size="small"
|
||||
@click="processEvent('rephrase')"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,6 +74,7 @@ import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, clickaway],
|
||||
@@ -61,10 +87,18 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isPrivateNote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isGenerating: false,
|
||||
uiFlags: {
|
||||
rephrase: false,
|
||||
reply_suggestion: false,
|
||||
summarize: false,
|
||||
},
|
||||
showDropdown: false,
|
||||
activeTone: 'professional',
|
||||
tones: [
|
||||
@@ -94,7 +128,7 @@ export default {
|
||||
).hooks[0].id;
|
||||
},
|
||||
buttonText() {
|
||||
return this.isGenerating
|
||||
return this.uiFlags.isRephrasing
|
||||
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
|
||||
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
|
||||
},
|
||||
@@ -111,24 +145,35 @@ export default {
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
},
|
||||
async processText() {
|
||||
this.isGenerating = true;
|
||||
async recordAnalytics({ type, tone }) {
|
||||
const event = OPEN_AI_EVENTS[type.toUpperCase()];
|
||||
if (event) {
|
||||
this.$track(event, {
|
||||
type,
|
||||
tone,
|
||||
});
|
||||
}
|
||||
},
|
||||
async processEvent(type = 'rephrase') {
|
||||
this.uiFlags[type] = true;
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
hookId: this.hookId,
|
||||
type: 'rephrase',
|
||||
type,
|
||||
content: this.message,
|
||||
tone: this.activeTone,
|
||||
conversationId: this.conversationId,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
} = result;
|
||||
this.$emit('replace-text', generatedMessage || this.message);
|
||||
this.closeDropdown();
|
||||
this.recordAnalytics({ type, tone: this.activeTone });
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
this.uiFlags[type] = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,15 +13,19 @@ import videojs from 'video.js';
|
||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
||||
|
||||
import Recorder from 'opus-recorder';
|
||||
|
||||
// Workers to record Audio .ogg and .wav
|
||||
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
|
||||
import waveWorker from 'opus-recorder/dist/waveWorker.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-wavesurfer/dist/videojs.wavesurfer.js';
|
||||
import 'videojs-record/dist/videojs.record.js';
|
||||
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
|
||||
|
||||
import OpusRecorderEngine from 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
|
||||
|
||||
import { format, addSeconds } from 'date-fns';
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
|
||||
@@ -33,7 +37,7 @@ export default {
|
||||
props: {
|
||||
audioRecordFormat: {
|
||||
type: String,
|
||||
default: AUDIO_FORMATS.WEBM,
|
||||
default: AUDIO_FORMATS.WAV,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
@@ -79,24 +83,18 @@ export default {
|
||||
maxLength: 900,
|
||||
timeSlice: 1000,
|
||||
maxFileSize: 15 * 1024 * 1024,
|
||||
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && {
|
||||
monitorGain: 0,
|
||||
recordingGain: 1,
|
||||
numberOfChannels: 1,
|
||||
encoderSampleRate: 16000,
|
||||
originalSampleRateOverride: 16000,
|
||||
streamPages: true,
|
||||
maxFramesPerPage: 1,
|
||||
encoderFrameSize: 1,
|
||||
encoderPath: waveWorker,
|
||||
displayMilliseconds: false,
|
||||
audioChannels: 1,
|
||||
audioSampleRate: 48000,
|
||||
audioBitRate: 128,
|
||||
audioEngine: 'opus-recorder',
|
||||
...(this.audioRecordFormat === AUDIO_FORMATS.WAV && {
|
||||
audioMimeType: 'audio/wav',
|
||||
audioWorkerURL: waveWorker,
|
||||
}),
|
||||
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
|
||||
displayMilliseconds: false,
|
||||
audioEngine: 'opus-recorder',
|
||||
audioMimeType: 'audio/ogg',
|
||||
audioWorkerURL: encoderWorker,
|
||||
audioChannels: 1,
|
||||
audioSampleRate: 48000,
|
||||
audioBitRate: 128,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -134,6 +132,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
deviceReady() {
|
||||
if (this.player.record().engine instanceof OpusRecorderEngine) {
|
||||
if (this.audioRecordFormat === AUDIO_FORMATS.WAV) {
|
||||
this.player.record().engine.audioType = 'audio/wav';
|
||||
}
|
||||
}
|
||||
this.player.record().start();
|
||||
},
|
||||
startRecord() {
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
const createState = (content, placeholder, plugins = []) => {
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
</file-upload>
|
||||
<woot-button
|
||||
v-if="showAudioRecorderButton"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||
emoji="🎤"
|
||||
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
@click="toggleAudioRecorder"
|
||||
/>
|
||||
<woot-button
|
||||
@@ -92,8 +92,8 @@
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
<AIAssistanceButton
|
||||
v-if="message"
|
||||
:conversation-id="conversationId"
|
||||
:is-private-note="isOnPrivateNote"
|
||||
:message="message"
|
||||
@replace-text="replaceText"
|
||||
/>
|
||||
|
||||
@@ -45,6 +45,14 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
collection() {
|
||||
this.renderChart(this.collection, {
|
||||
...chartOptions,
|
||||
...this.chartOptions,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.collection, {
|
||||
...chartOptions,
|
||||
|
||||
@@ -38,8 +38,8 @@ export default {
|
||||
this.onTabChange();
|
||||
},
|
||||
onTabChange() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
this.$emit('statusFilterChange', this.activeStatus);
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$emit('onChangeFilter', this.activeStatus);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+79
-11
@@ -1,9 +1,21 @@
|
||||
<template>
|
||||
<div class="column">
|
||||
<woot-modal-header :header-title="$t('FILTER.TITLE')">
|
||||
<p>{{ $t('FILTER.SUBTITLE') }}</p>
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p>{{ filterModalSubTitle }}</p>
|
||||
</woot-modal-header>
|
||||
<div class="row modal-content">
|
||||
<div class="column modal-content">
|
||||
<div v-if="isFolderView" class="columns">
|
||||
<label class="input-label" :class="{ error: !activeFolderNewName }">
|
||||
{{ $t('FILTER.FOLDER_LABEL') }}
|
||||
<input v-model="activeFolderNewName" type="text" class="name-input" />
|
||||
<span v-if="!activeFolderNewName" class="message">
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('FILTER.FOLDER_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="medium-12 columns filters-wrap">
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
@@ -42,7 +54,14 @@
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitFilterQuery">
|
||||
<woot-button
|
||||
v-if="isFolderView"
|
||||
:disabled="!activeFolderNewName"
|
||||
@click="updateSavedCustomViews"
|
||||
>
|
||||
{{ $t('FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
@@ -81,6 +100,14 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeFolderName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isFolderView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
@@ -107,6 +134,7 @@ export default {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: this.initialAppliedFilters,
|
||||
activeFolderNewName: this.activeFolderName,
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterAttributeGroups,
|
||||
filterGroups: [],
|
||||
@@ -119,6 +147,16 @@ export default {
|
||||
...mapGetters({
|
||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
filterModalHeaderTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.TITLE')
|
||||
: this.$t('FILTER.EDIT_CUSTOM_FILTER');
|
||||
},
|
||||
filterModalSubTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.SUBTITLE')
|
||||
: this.$t('FILTER.CUSTOM_VIEWS_SUBTITLE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
@@ -126,7 +164,7 @@ export default {
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [];
|
||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
||||
} else {
|
||||
} else if (!this.isFolderView) {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
@@ -177,11 +215,11 @@ export default {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.inputType;
|
||||
return type?.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.filterOperators;
|
||||
return type?.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
@@ -267,11 +305,30 @@ export default {
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
if (this.isFolderView) {
|
||||
this.setQueryOperatorOnLastQuery();
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
setQueryOperatorOnLastQuery() {
|
||||
const lastItemIndex = this.appliedFilters.length - 1;
|
||||
this.appliedFilters[lastItemIndex] = {
|
||||
...this.appliedFilters[lastItemIndex],
|
||||
query_operator: 'and',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
@@ -296,6 +353,9 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
updateSavedCustomViews() {
|
||||
this.$emit('updateFolder', this.appliedFilters, this.activeFolderNewName);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
@@ -322,4 +382,12 @@ export default {
|
||||
.filter-actions {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
.name-input {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="position-relative">
|
||||
<woot-button
|
||||
v-tooltip.right="$t('CHAT_LIST.SORT_TOOLTIP_LABEL')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class="selector-button"
|
||||
icon="sort-icon"
|
||||
@click="toggleDropdown"
|
||||
/>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open basic-filter"
|
||||
>
|
||||
<div class="filter__item">
|
||||
<span>{{ this.$t('CHAT_LIST.CHAT_SORT.STATUS') }}</span>
|
||||
<filter-item
|
||||
type="status"
|
||||
:selected-value="chatStatus"
|
||||
:items="chatStatusItems"
|
||||
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
|
||||
@onChangeFilter="onChangeFilter"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter__item">
|
||||
<span>{{ this.$t('CHAT_LIST.CHAT_SORT.ORDER_BY') }}</span>
|
||||
<filter-item
|
||||
type="sort"
|
||||
:selected-value="chatSortFilter"
|
||||
:items="chatSortItems"
|
||||
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
|
||||
@onChangeFilter="onChangeFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import FilterItem from './FilterItem';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
|
||||
chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatStatusFilter: 'getChatStatusFilter',
|
||||
chatSortFilter: 'getChatSortFilter',
|
||||
}),
|
||||
chatStatus() {
|
||||
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
sortFilter() {
|
||||
return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTabChange(value) {
|
||||
this.$emit('changeFilter', value);
|
||||
this.closeDropdown();
|
||||
},
|
||||
toggleDropdown() {
|
||||
this.showActionsDropdown = !this.showActionsDropdown;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
onChangeFilter(type, value) {
|
||||
this.$emit('changeFilter', type, value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.basic-filter {
|
||||
margin-top: var(--space-smaller);
|
||||
padding: var(--space-normal);
|
||||
right: 0;
|
||||
width: 21rem;
|
||||
|
||||
span {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.filter__item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:last-child {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
}"
|
||||
@mouseenter="onCardHover"
|
||||
@mouseleave="onCardLeave"
|
||||
@click="cardClick(chat)"
|
||||
@click="onCardClick"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
>
|
||||
<label v-if="hovered || selected" class="checkbox-wrapper" @click.stop>
|
||||
@@ -313,21 +313,33 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
const path = conversationUrl({
|
||||
accountId: this.accountId,
|
||||
activeInbox,
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
});
|
||||
onCardClick(e) {
|
||||
const { activeInbox, chat } = this;
|
||||
const path = frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
activeInbox,
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
})
|
||||
);
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(
|
||||
window.chatwootConfig.hostURL + path,
|
||||
'_blank',
|
||||
'noopener noreferrer nofollow'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.isActiveChat) {
|
||||
return;
|
||||
}
|
||||
router.push({ path: frontendURL(path) });
|
||||
|
||||
router.push({ path });
|
||||
},
|
||||
onCardHover() {
|
||||
this.hovered = !this.hideThumbnail;
|
||||
@@ -443,11 +455,11 @@ export default {
|
||||
.conversation-metadata-attributes {
|
||||
display: flex;
|
||||
gap: var(--space-small);
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
display: inline-flex;
|
||||
margin-left: var(--space-small);
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<select v-model="activeValue" class="status--filter" @change="onTabChange()">
|
||||
<option v-for="(value, status) in items" :key="status" :value="status">
|
||||
{{ $t(`${pathPrefix}.${status}.TEXT`) }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
selectedValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pathPrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeValue: this.selectedValue,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onTabChange() {
|
||||
if (this.type === 'status') {
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeValue);
|
||||
} else {
|
||||
this.$store.dispatch('setChatSortFilter', this.activeValue);
|
||||
}
|
||||
this.$emit('onChangeFilter', this.activeValue, this.type);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.status--filter {
|
||||
background-color: var(--color-background-light);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-mini);
|
||||
height: var(--space-medium);
|
||||
margin: 0 var(--space-smaller);
|
||||
padding: 0 var(--space-medium) 0 var(--space-small);
|
||||
width: 126px;
|
||||
}
|
||||
</style>
|
||||
@@ -22,6 +22,18 @@
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<blockquote v-if="storyReply" class="story-reply-quote">
|
||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||
<bubble-image
|
||||
v-if="!hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
@error="onStoryLoadError"
|
||||
/>
|
||||
<bubble-video
|
||||
v-else-if="hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
/>
|
||||
</blockquote>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
@@ -41,22 +53,11 @@
|
||||
</span>
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||
:url="attachment.data_url"
|
||||
<bubble-image-audio-video
|
||||
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
:attachment="attachment"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="attachment.file_type === 'audio'"
|
||||
controls
|
||||
class="skip-context-menu"
|
||||
>
|
||||
<source :src="attachment.data_url" />
|
||||
</audio>
|
||||
<bubble-video
|
||||
v-else-if="attachment.file_type === 'video'"
|
||||
:url="attachment.data_url"
|
||||
/>
|
||||
<bubble-location
|
||||
v-else-if="attachment.file_type === 'location'"
|
||||
:latitude="attachment.coordinates_lat"
|
||||
@@ -79,7 +80,7 @@
|
||||
:sender="data.sender"
|
||||
:story-sender="storySender"
|
||||
:external-error="externalError"
|
||||
:story-id="storyId"
|
||||
:story-id="`${storyId}`"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
@@ -132,11 +133,12 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import BubbleFile from './bubble/File';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleVideo from './bubble/Video';
|
||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo';
|
||||
import BubbleIntegration from './bubble/Integration.vue';
|
||||
import BubbleLocation from './bubble/Location';
|
||||
import BubbleMailHead from './bubble/MailHead';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleVideo from './bubble/Video.vue';
|
||||
import BubbleContact from './bubble/Contact';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||
@@ -153,11 +155,12 @@ export default {
|
||||
BubbleActions,
|
||||
BubbleFile,
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
BubbleImageAudioVideo,
|
||||
BubbleIntegration,
|
||||
BubbleLocation,
|
||||
BubbleMailHead,
|
||||
BubbleText,
|
||||
BubbleVideo,
|
||||
BubbleContact,
|
||||
ContextMenu,
|
||||
Spinner,
|
||||
@@ -192,6 +195,7 @@ export default {
|
||||
hasImageError: false,
|
||||
contextMenuPosition: {},
|
||||
showBackgroundHighlight: false,
|
||||
hasImgStoryError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -277,6 +281,12 @@ export default {
|
||||
storyId() {
|
||||
return this.contentAttributes.story_id || null;
|
||||
},
|
||||
storyUrl() {
|
||||
return this.contentAttributes.story_url || null;
|
||||
},
|
||||
storyReply() {
|
||||
return this.storyUrl && this.hasInstagramStory;
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
@@ -414,10 +424,12 @@ export default {
|
||||
watch: {
|
||||
data() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.setupHighlightTimer();
|
||||
},
|
||||
@@ -426,12 +438,18 @@ export default {
|
||||
clearTimeout(this.higlightTimeout);
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video'].includes(fileType);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasImageError;
|
||||
}
|
||||
if (this.storyReply) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleContextMenuClick() {
|
||||
@@ -443,6 +461,9 @@ export default {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onStoryLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
},
|
||||
openContextMenu(e) {
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
@@ -672,7 +693,6 @@ li.right {
|
||||
blockquote {
|
||||
border-left: var(--space-micro) solid var(--s-75);
|
||||
color: var(--s-800);
|
||||
padding: var(--space-smaller) var(--space-small);
|
||||
margin: var(--space-smaller) 0;
|
||||
padding: var(--space-small) var(--space-small) 0 var(--space-normal);
|
||||
}
|
||||
@@ -704,4 +724,11 @@ li.right {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.story-reply-quote {
|
||||
border-left: var(--space-micro) solid var(--s-75);
|
||||
color: var(--s-600);
|
||||
margin: var(--space-small) var(--space-normal) 0;
|
||||
padding: var(--space-small) var(--space-small) 0 var(--space-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
v-for="message in getReadMessages"
|
||||
:key="message.id"
|
||||
class="message--read ph-no-capture"
|
||||
data-clarity-mask="True"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
@@ -56,6 +57,7 @@
|
||||
v-for="message in getUnReadMessages"
|
||||
:key="message.id"
|
||||
class="message--unread ph-no-capture"
|
||||
data-clarity-mask="True"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
@@ -93,7 +95,9 @@ import { mapGetters } from 'vuex';
|
||||
|
||||
import ReplyBox from './ReplyBox';
|
||||
import Message from './Message';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import conversationMixin, {
|
||||
filterDuplicateSourceMessages,
|
||||
} from '../../../mixins/conversations';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -171,24 +175,28 @@ export default {
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
getMessages() {
|
||||
const [chat] = this.allConversations.filter(
|
||||
c => c.id === this.currentChat.id
|
||||
);
|
||||
return chat;
|
||||
const messages = this.currentChat.messages || [];
|
||||
if (this.isAWhatsAppChannel) {
|
||||
return filterDuplicateSourceMessages(messages);
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
getReadMessages() {
|
||||
const chat = this.getMessages;
|
||||
return chat === undefined ? null : this.readMessages(chat);
|
||||
return this.readMessages(
|
||||
this.getMessages,
|
||||
this.currentChat.agent_last_seen_at
|
||||
);
|
||||
},
|
||||
getUnReadMessages() {
|
||||
const chat = this.getMessages;
|
||||
return chat === undefined ? null : this.unReadMessages(chat);
|
||||
return this.unReadMessages(
|
||||
this.getMessages,
|
||||
this.currentChat.agent_last_seen_at
|
||||
);
|
||||
},
|
||||
shouldShowSpinner() {
|
||||
return (
|
||||
(this.getMessages && this.getMessages.dataFetched === undefined) ||
|
||||
(this.currentChat && this.currentChat.dataFetched === undefined) ||
|
||||
(!this.listLoadingStatus && this.isLoadingPrevious)
|
||||
);
|
||||
},
|
||||
@@ -208,7 +216,7 @@ export default {
|
||||
|
||||
selectedTweet() {
|
||||
if (this.selectedTweetId) {
|
||||
const { messages = [] } = this.getMessages;
|
||||
const { messages = [] } = this.currentChat;
|
||||
const [selectedMessage] = messages.filter(
|
||||
message => message.id === this.selectedTweetId
|
||||
);
|
||||
@@ -271,6 +279,7 @@ export default {
|
||||
if (newChat.id === oldChat.id) {
|
||||
return;
|
||||
}
|
||||
this.fetchAllAttachmentsFromCurrentChat();
|
||||
this.selectedTweetId = null;
|
||||
},
|
||||
},
|
||||
@@ -282,6 +291,7 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.addScrollListener();
|
||||
this.fetchAllAttachmentsFromCurrentChat();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
@@ -290,6 +300,9 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchAllAttachmentsFromCurrentChat() {
|
||||
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
|
||||
},
|
||||
removeBusListeners() {
|
||||
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
||||
@@ -360,7 +373,7 @@ export default {
|
||||
async fetchPreviousMessages(scrollTop = 0) {
|
||||
this.setScrollParams();
|
||||
const shouldLoadMoreMessages =
|
||||
this.getMessages.dataFetched === true &&
|
||||
this.currentChat.dataFetched === true &&
|
||||
!this.listLoadingStatus &&
|
||||
!this.isLoadingPrevious;
|
||||
|
||||
@@ -373,7 +386,7 @@ export default {
|
||||
try {
|
||||
await this.$store.dispatch('fetchPreviousMessages', {
|
||||
conversationId: this.currentChat.id,
|
||||
before: this.getMessages.messages[0].id,
|
||||
before: this.currentChat.messages[0].id,
|
||||
});
|
||||
const heightDifference =
|
||||
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
|
||||
|
||||
@@ -48,15 +48,17 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.conversation-priority-mark {
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
background: var(--s-50);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--s-600);
|
||||
display: inline-flex;
|
||||
width: var(--space-snug);
|
||||
height: var(--space-snug);
|
||||
|
||||
&.urgent {
|
||||
background: var(--r-500);
|
||||
color: var(--w-25);
|
||||
background: var(--r-50);
|
||||
color: var(--r-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -164,11 +164,11 @@ import {
|
||||
AUDIO_FORMATS,
|
||||
} from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
|
||||
import {
|
||||
getMessageVariables,
|
||||
getUndefinedVariablesInMessage,
|
||||
} from 'dashboard/helper/messageHelper';
|
||||
replaceVariablesInMessage,
|
||||
} from '@chatwoot/utils';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
@@ -501,10 +501,10 @@ export default {
|
||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
},
|
||||
audioRecordFormat() {
|
||||
if (this.isAWebWidgetInbox) {
|
||||
return AUDIO_FORMATS.WEBM;
|
||||
if (this.isAWhatsAppChannel || this.isAPIInbox) {
|
||||
return AUDIO_FORMATS.OGG;
|
||||
}
|
||||
return AUDIO_FORMATS.OGG;
|
||||
return AUDIO_FORMATS.WAV;
|
||||
},
|
||||
messageVariables() {
|
||||
const variables = getMessageVariables({
|
||||
|
||||
@@ -195,7 +195,7 @@ export default {
|
||||
return '';
|
||||
}
|
||||
const { storySender, storyId } = this;
|
||||
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
||||
return `https://www.instagram.com/stories/direct/${storySender}_${storyId}`;
|
||||
},
|
||||
showStatusIndicators() {
|
||||
if ((this.isOutgoing || this.isTemplate) && !this.isPrivate) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="message-text__wrap" :class="attachmentTypeClasses">
|
||||
<img
|
||||
v-if="isImage && !isImageError"
|
||||
:src="attachment.data_url"
|
||||
@click="onClick"
|
||||
@error="onImgError()"
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:src="attachment.data_url"
|
||||
muted
|
||||
playsInline
|
||||
@click="onClick"
|
||||
/>
|
||||
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
||||
<source :src="`${attachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
<gallery-view
|
||||
v-if="show"
|
||||
:show.sync="show"
|
||||
:attachment="attachment"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onImgError()"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { hasPressedCommand } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
import GalleryView from '../components/GalleryView';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GalleryView,
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
isImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChatAttachments: 'getSelectedChatAttachments',
|
||||
}),
|
||||
isImage() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
attachmentTypeClasses() {
|
||||
return {
|
||||
image: this.isImage,
|
||||
video: this.isVideo,
|
||||
};
|
||||
},
|
||||
filteredCurrentChatAttachments() {
|
||||
const attachments = this.currentChatAttachments.filter(attachment =>
|
||||
['image', 'video', 'audio'].includes(attachment.file_type)
|
||||
);
|
||||
return attachments;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachment() {
|
||||
this.isImageError = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
onClick(e) {
|
||||
if (hasPressedCommand(e)) {
|
||||
window.open(this.attachment.data_url, '_blank');
|
||||
return;
|
||||
}
|
||||
this.show = true;
|
||||
},
|
||||
onImgError() {
|
||||
this.isImageError = true;
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<woot-modal full-width :show.sync="show" :on-close="onClose">
|
||||
<div v-on-clickaway="onClose" class="gallery-modal--wrap" @click="onClose">
|
||||
<div class="attachment-toggle--button">
|
||||
<woot-button
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
size="large"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="chevron-left"
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="attachments-viewer">
|
||||
<div class="attachment-view">
|
||||
<img
|
||||
v-if="isImage"
|
||||
:key="attachmentSrc"
|
||||
:src="attachmentSrc"
|
||||
class="modal-image skip-context-menu"
|
||||
@click.stop
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:key="attachmentSrc"
|
||||
:src="attachmentSrc"
|
||||
controls
|
||||
playsInline
|
||||
class="modal-video skip-context-menu"
|
||||
@click.stop
|
||||
/>
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="attachmentSrc"
|
||||
controls
|
||||
class="skip-context-menu"
|
||||
@click.stop
|
||||
>
|
||||
<source :src="`${attachmentSrc}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-toggle--button">
|
||||
<woot-button
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
size="large"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
icon="chevron-right"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import {
|
||||
isEscape,
|
||||
hasPressedArrowLeftKey,
|
||||
hasPressedArrowRightKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins, clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
allAttachments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attachmentSrc: '',
|
||||
activeFileType: '',
|
||||
activeImageIndex:
|
||||
this.allAttachments.findIndex(
|
||||
attachment => attachment.id === this.attachment.id
|
||||
) || 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasMoreThanOneAttachment() {
|
||||
return this.allAttachments.length > 1;
|
||||
},
|
||||
isImage() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setImageAndVideoSrc(this.attachment);
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onClickChangeAttachment(attachment, index) {
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
this.activeImageIndex = index;
|
||||
this.setImageAndVideoSrc(attachment);
|
||||
},
|
||||
setImageAndVideoSrc(attachment) {
|
||||
const { file_type: type } = attachment;
|
||||
if (!Object.values(ALLOWED_FILE_TYPES).includes(type)) {
|
||||
return;
|
||||
}
|
||||
this.attachmentSrc = attachment.data_url;
|
||||
this.activeFileType = type;
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
if (isEscape(e)) {
|
||||
this.onClose();
|
||||
} else if (hasPressedArrowLeftKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
} else if (hasPressedArrowRightKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.gallery-modal--wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
.attachments-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.attachment-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-toggle--button {
|
||||
width: var(--space-mega);
|
||||
min-width: var(--space-mega);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -79,6 +79,7 @@ import MenuItem from './menuItem.vue';
|
||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import snoozeTimesMixin from 'dashboard/mixins/conversation/snoozeTimesMixin';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||
export default {
|
||||
@@ -87,7 +88,7 @@ export default {
|
||||
MenuItemWithSubmenu,
|
||||
AgentLoadingPlaceholder,
|
||||
},
|
||||
mixins: [snoozeTimesMixin],
|
||||
mixins: [snoozeTimesMixin, agentMixin],
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
@@ -202,6 +203,16 @@ export default {
|
||||
teams: 'teams/getTeams',
|
||||
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
filteredAgentOnAvailability() {
|
||||
const agents = this.$store.getters[
|
||||
'inboxAssignableAgents/getAssignableAgents'
|
||||
](this.inboxId);
|
||||
const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents);
|
||||
const filteredAgents = this.sortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
return filteredAgents;
|
||||
},
|
||||
assignableAgents() {
|
||||
return [
|
||||
{
|
||||
@@ -212,9 +223,7 @@ export default {
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
},
|
||||
...this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||
this.inboxId
|
||||
),
|
||||
...this.filteredAgentOnAvailability,
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -246,6 +255,7 @@ export default {
|
||||
...(type === 'icon' && { icon: option.icon }),
|
||||
...(type === 'label' && { color: option.color }),
|
||||
...(type === 'agent' && { thumbnail: option.thumbnail }),
|
||||
...(type === 'agent' && { status: option.availability_status }),
|
||||
...(type === 'text' && { label: option.label }),
|
||||
...(type === 'label' && { label: option.title }),
|
||||
...(type === 'agent' && { label: option.name }),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
v-if="variant === 'agent'"
|
||||
:username="option.label"
|
||||
:src="option.thumbnail"
|
||||
:status="option.status"
|
||||
size="20px"
|
||||
class="agent-thumbnail"
|
||||
/>
|
||||
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-on-clickaway="onCloseAgentList" class="bulk-action__agents">
|
||||
<div class="triangle" :style="cssVars">
|
||||
<div class="triangle">
|
||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||
<path
|
||||
d="M20 12l-8-8-12 12"
|
||||
@@ -104,14 +104,13 @@ import { mapGetters } from 'vuex';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [clickaway, bulkActionsMixin],
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
selectedInboxes: {
|
||||
type: Array,
|
||||
@@ -240,7 +239,7 @@ export default {
|
||||
display: block;
|
||||
z-index: var(--z-index-one);
|
||||
position: absolute;
|
||||
top: calc(var(--space-slab) * -1);
|
||||
top: var(--space-minus-slab);
|
||||
right: var(--triangle-position);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
+17
-4
@@ -58,7 +58,7 @@
|
||||
<transition name="popover-animation">
|
||||
<label-actions
|
||||
v-if="showLabelActions"
|
||||
triangle-position="8.5"
|
||||
class="label-actions-box"
|
||||
@assign="assignLabels"
|
||||
@close="showLabelActions = false"
|
||||
/>
|
||||
@@ -66,12 +66,12 @@
|
||||
<transition name="popover-animation">
|
||||
<update-actions
|
||||
v-if="showUpdateActions"
|
||||
class="update-actions-box"
|
||||
:selected-inboxes="selectedInboxes"
|
||||
:conversation-count="conversations.length"
|
||||
:show-resolve="!showResolvedAction"
|
||||
:show-reopen="!showOpenAction"
|
||||
:show-snooze="!showSnoozedAction"
|
||||
triangle-position="5.6"
|
||||
@update="updateConversations"
|
||||
@close="showUpdateActions = false"
|
||||
/>
|
||||
@@ -79,9 +79,9 @@
|
||||
<transition name="popover-animation">
|
||||
<agent-selector
|
||||
v-if="showAgentsList"
|
||||
class="agent-actions-box"
|
||||
:selected-inboxes="selectedInboxes"
|
||||
:conversation-count="conversations.length"
|
||||
triangle-position="2.8"
|
||||
@select="submit"
|
||||
@close="showAgentsList = false"
|
||||
/>
|
||||
@@ -89,7 +89,7 @@
|
||||
<transition name="popover-animation">
|
||||
<team-actions
|
||||
v-if="showTeamsList"
|
||||
triangle-position="0.2"
|
||||
class="team-actions-box"
|
||||
@assign-team="assignTeam"
|
||||
@close="showTeamsList = false"
|
||||
/>
|
||||
@@ -247,4 +247,17 @@ export default {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.label-actions-box {
|
||||
--triangle-position: 8.5rem;
|
||||
}
|
||||
.update-actions-box {
|
||||
--triangle-position: 5.6rem;
|
||||
}
|
||||
.agent-actions-box {
|
||||
--triangle-position: 2.8rem;
|
||||
}
|
||||
.team-actions-box {
|
||||
--triangle-position: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-on-clickaway="onClose" class="labels-container">
|
||||
<div class="triangle" :style="cssVars">
|
||||
<div class="triangle">
|
||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||
<path
|
||||
d="M20 12l-8-8-12 12"
|
||||
@@ -75,10 +75,9 @@
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [clickaway, bulkActionsMixin],
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
@@ -207,7 +206,7 @@ export default {
|
||||
position: absolute;
|
||||
right: var(--triangle-position);
|
||||
text-align: left;
|
||||
top: calc(var(--space-slab) * -1);
|
||||
top: var(--space-minus-slab);
|
||||
z-index: var(--z-index-one);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-on-clickaway="onClose" class="bulk-action__teams">
|
||||
<div class="triangle" :style="cssVars">
|
||||
<div class="triangle">
|
||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||
<path
|
||||
d="M20 12l-8-8-12 12"
|
||||
@@ -59,9 +59,8 @@
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||
export default {
|
||||
mixins: [clickaway, bulkActionsMixin],
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
@@ -141,7 +140,7 @@ export default {
|
||||
display: block;
|
||||
z-index: var(--z-index-one);
|
||||
position: absolute;
|
||||
top: calc(var(--space-slab) * -1);
|
||||
top: var(--space-minus-slab);
|
||||
right: var(--triangle-position);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-on-clickaway="onClose" class="actions-container">
|
||||
<div class="triangle" :style="cssVars">
|
||||
<div class="triangle">
|
||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||
<path
|
||||
d="M20 12l-8-8-12 12"
|
||||
@@ -45,14 +45,13 @@
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
},
|
||||
mixins: [clickaway, bulkActionsMixin],
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
selectedInboxes: {
|
||||
type: Array,
|
||||
@@ -156,7 +155,7 @@ export default {
|
||||
position: absolute;
|
||||
right: var(--triangle-position);
|
||||
text-align: left;
|
||||
top: calc(var(--space-slab) * -1);
|
||||
top: var(--space-minus-slab);
|
||||
z-index: var(--z-index-one);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ export default {
|
||||
SNOOZED: 'snoozed',
|
||||
ALL: 'all',
|
||||
},
|
||||
SORT_BY_TYPE: {
|
||||
LATEST: 'latest',
|
||||
CREATED_AT: 'sort_on_created_at',
|
||||
PRIORITY: 'sort_on_priority',
|
||||
},
|
||||
ARTICLE_STATUS_TYPES: {
|
||||
DRAFT: 0,
|
||||
PUBLISH: 1,
|
||||
|
||||
@@ -15,4 +15,5 @@ export const FEATURE_FLAGS = {
|
||||
REPORTS: 'reports',
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
AUDIT_LOGS: 'audit_logs',
|
||||
};
|
||||
|
||||
@@ -76,3 +76,9 @@ export const PORTALS_EVENTS = Object.freeze({
|
||||
DELETE_ARTICLE: 'Deleted an article',
|
||||
PREVIEW_ARTICLE: 'Previewed article',
|
||||
});
|
||||
|
||||
export const OPEN_AI_EVENTS = Object.freeze({
|
||||
SUMMARIZE: 'OpenAI: Used summarize',
|
||||
REPLY_SUGGESTION: 'OpenAI: Used reply suggestion',
|
||||
REPHRASE: 'OpenAI: Used rephrase',
|
||||
});
|
||||
|
||||
@@ -105,8 +105,16 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
onLogout = () => AuthAPI.logout();
|
||||
|
||||
onMessageCreated = data => {
|
||||
const {
|
||||
conversation: { last_activity_at: lastActivityAt },
|
||||
conversation_id: conversationId,
|
||||
} = data;
|
||||
DashboardAudioNotificationHelper.onNewMessage(data);
|
||||
this.app.$store.dispatch('addMessage', data);
|
||||
this.app.$store.dispatch('updateConversationLastActivity', {
|
||||
lastActivityAt,
|
||||
conversationId,
|
||||
});
|
||||
};
|
||||
|
||||
onReload = () => window.location.reload();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
export const getInputType = (key, operator, filterTypes) => {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
};
|
||||
|
||||
export const generateCustomAttributesInputType = type => {
|
||||
const filterInputTypes = {
|
||||
text: 'string',
|
||||
number: 'string',
|
||||
date: 'string',
|
||||
checkbox: 'multi_select',
|
||||
list: 'multi_select',
|
||||
link: 'string',
|
||||
};
|
||||
return filterInputTypes[type];
|
||||
};
|
||||
|
||||
export const getAttributeInputType = (key, allCustomAttributes) => {
|
||||
const customAttribute = allCustomAttributes.find(
|
||||
attr => attr.attribute_key === key
|
||||
);
|
||||
const { attribute_display_type } = customAttribute;
|
||||
const filterInputTypes = generateCustomAttributesInputType(
|
||||
attribute_display_type
|
||||
);
|
||||
return filterInputTypes;
|
||||
};
|
||||
|
||||
export const getValuesName = (values, list, idKey, nameKey) => {
|
||||
const item = list?.find(v => v[idKey] === values[0]);
|
||||
return {
|
||||
id: values[0],
|
||||
name: item ? item[nameKey] : values[0],
|
||||
};
|
||||
};
|
||||
|
||||
const getValuesForLabels = (values, labels) => {
|
||||
const selectedLabels = labels.filter(label => values.includes(label.title));
|
||||
return selectedLabels.map(({ title }) => ({
|
||||
id: title,
|
||||
name: title,
|
||||
}));
|
||||
};
|
||||
|
||||
const getValuesForLanguages = (values, languages) => {
|
||||
const selectedLanguages = languages.filter(language =>
|
||||
values.includes(language.id)
|
||||
);
|
||||
return selectedLanguages.map(({ id, name }) => ({
|
||||
id: id.toLowerCase(),
|
||||
name: name,
|
||||
}));
|
||||
};
|
||||
|
||||
const getValuesForCountries = (values, countries) => {
|
||||
const selectedCountries = countries.filter(country =>
|
||||
values.includes(country.id)
|
||||
);
|
||||
return selectedCountries.map(({ id, name }) => ({
|
||||
id: id,
|
||||
name: name,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getValuesForFilter = (filter, params) => {
|
||||
const { attribute_key, values } = filter;
|
||||
const {
|
||||
languages,
|
||||
countries,
|
||||
agents,
|
||||
inboxes,
|
||||
teams,
|
||||
campaigns,
|
||||
labels,
|
||||
} = params;
|
||||
switch (attribute_key) {
|
||||
case 'assignee_id':
|
||||
return getValuesName(values, agents, 'id', 'name');
|
||||
case 'inbox_id':
|
||||
return getValuesName(values, inboxes, 'id', 'name');
|
||||
case 'team_id':
|
||||
return getValuesName(values, teams, 'id', 'name');
|
||||
case 'campaign_id':
|
||||
return getValuesName(values, campaigns, 'id', 'title');
|
||||
case 'labels': {
|
||||
return getValuesForLabels(values, labels);
|
||||
}
|
||||
case 'browser_language': {
|
||||
return getValuesForLanguages(values, languages);
|
||||
}
|
||||
case 'country_code': {
|
||||
return getValuesForCountries(values, countries);
|
||||
}
|
||||
default:
|
||||
return { id: values[0], name: values[0] };
|
||||
}
|
||||
};
|
||||
|
||||
export const generateValuesForEditCustomViews = (filter, params) => {
|
||||
const { attribute_key, filter_operator, values } = filter;
|
||||
const { filterTypes, allCustomAttributes } = params;
|
||||
const inboxType = getInputType(attribute_key, filter_operator, filterTypes);
|
||||
|
||||
if (inboxType === undefined) {
|
||||
const filterInputTypes = getAttributeInputType(
|
||||
attribute_key,
|
||||
allCustomAttributes
|
||||
);
|
||||
return filterInputTypes === 'string'
|
||||
? values[0].toString()
|
||||
: { id: values[0], name: values[0] };
|
||||
}
|
||||
|
||||
return inboxType === 'multi_select' || inboxType === 'search_select'
|
||||
? getValuesForFilter(filter, params)
|
||||
: values[0].toString();
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
const setArrayValues = item => {
|
||||
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
|
||||
};
|
||||
const generatePayload = data => {
|
||||
// Make a copy of data to avoid vue data reactivity issues
|
||||
const filters = JSON.parse(JSON.stringify(data));
|
||||
let payload = filters.map(item => {
|
||||
if (Array.isArray(item.values)) {
|
||||
item.values = item.values.map(val => val.id);
|
||||
item.values = setArrayValues(item);
|
||||
} else if (typeof item.values === 'object') {
|
||||
item.values = [item.values.id];
|
||||
} else if (!item.values) {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
const MESSAGE_VARIABLES_REGEX = /{{(.*?)}}/g;
|
||||
export const replaceVariablesInMessage = ({ message, variables }) => {
|
||||
return message.replace(MESSAGE_VARIABLES_REGEX, (match, replace) => {
|
||||
return variables[replace.trim()]
|
||||
? variables[replace.trim().toLowerCase()]
|
||||
: '';
|
||||
});
|
||||
};
|
||||
|
||||
export const capitalizeName = name => {
|
||||
return name.replace(/\b(\w)/g, s => s.toUpperCase());
|
||||
};
|
||||
|
||||
const skipCodeBlocks = str => str.replace(/```(?:.|\n)+?```/g, '');
|
||||
|
||||
export const getFirstName = ({ user }) => {
|
||||
const firstName = user?.name ? user.name.split(' ').shift() : '';
|
||||
return capitalizeName(firstName);
|
||||
};
|
||||
|
||||
export const getLastName = ({ user }) => {
|
||||
if (user && user.name) {
|
||||
const lastName =
|
||||
user.name.split(' ').length > 1 ? user.name.split(' ').pop() : '';
|
||||
return capitalizeName(lastName);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getMessageVariables = ({ conversation }) => {
|
||||
const {
|
||||
meta: { assignee = {}, sender = {} },
|
||||
id,
|
||||
} = conversation;
|
||||
|
||||
return {
|
||||
'contact.name': capitalizeName(sender?.name),
|
||||
'contact.first_name': getFirstName({ user: sender }),
|
||||
'contact.last_name': getLastName({ user: sender }),
|
||||
'contact.email': sender?.email,
|
||||
'contact.phone': sender?.phone_number,
|
||||
'contact.id': sender?.id,
|
||||
'conversation.id': id,
|
||||
'agent.name': capitalizeName(assignee?.name || ''),
|
||||
'agent.first_name': getFirstName({ user: assignee }),
|
||||
'agent.last_name': getLastName({ user: assignee }),
|
||||
'agent.email': assignee?.email ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
export const getUndefinedVariablesInMessage = ({ message, variables }) => {
|
||||
const messageWithOutCodeBlocks = skipCodeBlocks(message);
|
||||
const matches = messageWithOutCodeBlocks.match(MESSAGE_VARIABLES_REGEX);
|
||||
if (!matches) return [];
|
||||
|
||||
return matches
|
||||
.map(match => {
|
||||
return match
|
||||
.replace('{{', '')
|
||||
.replace('}}', '')
|
||||
.trim();
|
||||
})
|
||||
.filter(variable => {
|
||||
return !variables[variable];
|
||||
});
|
||||
};
|
||||
@@ -8,8 +8,8 @@ export const buildPortalArticleURL = (
|
||||
portalSlug,
|
||||
categorySlug,
|
||||
locale,
|
||||
articleId
|
||||
articleSlug
|
||||
) => {
|
||||
const portalURL = buildPortalURL(portalSlug);
|
||||
return `${portalURL}/${locale}/${categorySlug}/${articleId}`;
|
||||
return `${portalURL}/articles/${articleSlug}`;
|
||||
};
|
||||
|
||||
@@ -18,18 +18,18 @@ export const initializeAnalyticsEvents = () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
window.bus.$on(ANALYTICS_RESET, () => {});
|
||||
};
|
||||
|
||||
const initializeAudioAlerts = user => {
|
||||
// InitializeAudioNotifications
|
||||
const { ui_settings: uiSettings } = user || {};
|
||||
const {
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
enable_audio_alerts: audioAlertType,
|
||||
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
|
||||
notification_tone: audioAlertTone,
|
||||
} = uiSettings;
|
||||
// UI Settings can be undefined initally as we don't send the
|
||||
// entire payload for the user during the signup process.
|
||||
} = uiSettings || {};
|
||||
|
||||
DashboardAudioNotificationHelper.setInstanceValues({
|
||||
currentUserId: user.id,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user