Compare commits
233
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b64f02f6 | ||
|
|
678c00f254 | ||
|
|
7298002da7 | ||
|
|
cd31cc8e71 | ||
|
|
96b781b7fd | ||
|
|
1ce5cbe275 | ||
|
|
c607f09be0 | ||
|
|
33a6ad9f7e | ||
|
|
e662336eaf | ||
|
|
0158a55845 | ||
|
|
657843960c | ||
|
|
fc6a22b072 | ||
|
|
227d99934e | ||
|
|
5036b28e45 | ||
|
|
8b5735c915 | ||
|
|
eb379e1849 | ||
|
|
6a630bc489 | ||
|
|
0d1b474e60 | ||
|
|
84f6cadae7 | ||
|
|
372e740c38 | ||
|
|
57dd979a14 | ||
|
|
c1d07a5471 | ||
|
|
98eddd0532 | ||
|
|
1b21e0d429 | ||
|
|
7776b74126 | ||
|
|
3abb887445 | ||
|
|
a9ec3ae098 | ||
|
|
53927b5a72 | ||
|
|
d67b91d2b0 | ||
|
|
a4fc28abc0 | ||
|
|
bee2a14620 | ||
|
|
168a4dc323 | ||
|
|
47c0decf1e | ||
|
|
8acc818f68 | ||
|
|
9e0468cd73 | ||
|
|
65e9cee019 | ||
|
|
bb3851a35c | ||
|
|
4368bdb2bc | ||
|
|
1b753720c1 | ||
|
|
39e27d2a23 | ||
|
|
45e630fc60 | ||
|
|
0c35a77d4b | ||
|
|
07ea9694a3 | ||
|
|
85043e7d88 | ||
|
|
d3c1fce761 | ||
|
|
33e98bf61a | ||
|
|
cae7cb7002 | ||
|
|
b8047f0912 | ||
|
|
de98e434d6 | ||
|
|
74e5e2163a | ||
|
|
9464d4d647 | ||
|
|
b7a7e5a0d3 | ||
|
|
b9c62b3fed | ||
|
|
d10525a714 | ||
|
|
390cd756e8 | ||
|
|
ee3f734b7b | ||
|
|
905ca94f71 | ||
|
|
53d42b15b8 | ||
|
|
17cb788193 | ||
|
|
f2115b15f7 | ||
|
|
0805f362d3 | ||
|
|
eeb0113dc5 | ||
|
|
648c4caca1 | ||
|
|
2eeec22868 | ||
|
|
ef50edb9e2 | ||
|
|
3ed80fa867 | ||
|
|
cf664ca2a0 | ||
|
|
485c561b18 | ||
|
|
766698cb3a | ||
|
|
082793290a | ||
|
|
12916ceca6 | ||
|
|
3c952e6a4a | ||
|
|
59184122f7 | ||
|
|
381423b1ae | ||
|
|
b7c9f779ad | ||
|
|
904d76420d | ||
|
|
fa907840c7 | ||
|
|
3760f206e8 | ||
|
|
a861257f73 | ||
|
|
143299f138 | ||
|
|
232369cd5c | ||
|
|
4b40c61201 | ||
|
|
834c219b9b | ||
|
|
d0cd1c8887 | ||
|
|
682a2aea1c | ||
|
|
1dc66db516 | ||
|
|
bc04d81a5a | ||
|
|
d2c5c2f9a3 | ||
|
|
381fda270a | ||
|
|
b3c9d1f1a5 | ||
|
|
fd4376d062 | ||
|
|
a8f053921b | ||
|
|
8e15ada164 | ||
|
|
51093fd543 | ||
|
|
48e638cc56 | ||
|
|
e6dfc159ed | ||
|
|
900c2de89a | ||
|
|
29106ebc4b | ||
|
|
a579684b34 | ||
|
|
c29a9ad062 | ||
|
|
0ac015ce7a | ||
|
|
19474e0074 | ||
|
|
ce8190dacf | ||
|
|
aacf326ca1 | ||
|
|
e39c14460b | ||
|
|
5f6e17f307 | ||
|
|
4bf23adcf5 | ||
|
|
1f4d860d9d | ||
|
|
fdbb3bf4b1 | ||
|
|
c899cc825d | ||
|
|
eb972684b3 | ||
|
|
896473f03e | ||
|
|
8c43edbd50 | ||
|
|
4c4eb4bd90 | ||
|
|
88b8597e4d | ||
|
|
76fe2f9bb8 | ||
|
|
818424259f | ||
|
|
e67f8824d9 | ||
|
|
8f1a1e0905 | ||
|
|
1b6360d9d3 | ||
|
|
8b7f10fe61 | ||
|
|
4958b3e7b6 | ||
|
|
e467d15f2c | ||
|
|
d1e7e75638 | ||
|
|
4aab63b7df | ||
|
|
acf24649b0 | ||
|
|
316cf35795 | ||
|
|
52a5a59ddb | ||
|
|
22db201a08 | ||
|
|
ea7898d113 | ||
|
|
e7e14f01e4 | ||
|
|
7d6085cefd | ||
|
|
10076c6a3e | ||
|
|
1c44445088 | ||
|
|
d305c5fd0f | ||
|
|
1577288843 | ||
|
|
63b3686746 | ||
|
|
810f950e16 | ||
|
|
dd09d560d5 | ||
|
|
c60c523851 | ||
|
|
22c2235d90 | ||
|
|
35c26367da | ||
|
|
aaf4fee9a6 | ||
|
|
3c21f62485 | ||
|
|
50b2ca014e | ||
|
|
d731c972ad | ||
|
|
5845881b08 | ||
|
|
64138ef220 | ||
|
|
046ce68a45 | ||
|
|
e34ab5957f | ||
|
|
75a5492840 | ||
|
|
56fbbe92b4 | ||
|
|
dc4e13b300 | ||
|
|
2c7f93978e | ||
|
|
6e30064421 | ||
|
|
68062216e4 | ||
|
|
8a8f325f64 | ||
|
|
cf90b898f4 | ||
|
|
1f4cb0e1ca | ||
|
|
c6ecf80dff | ||
|
|
c29b77c214 | ||
|
|
c301845ade | ||
|
|
78fab7897d | ||
|
|
f48e014694 | ||
|
|
206433db32 | ||
|
|
1ddb73ea97 | ||
|
|
b5071e61d7 | ||
|
|
a80eff9aa3 | ||
|
|
a59fb90785 | ||
|
|
29478880ff | ||
|
|
c9f8cdb51b | ||
|
|
a2ebf92932 | ||
|
|
00eb5b152a | ||
|
|
0b2f539ad0 | ||
|
|
1ab3966b75 | ||
|
|
552d967c81 | ||
|
|
e5c8a68a76 | ||
|
|
15f50635e7 | ||
|
|
cf0d6dd7c6 | ||
|
|
fd1813949a | ||
|
|
6c480098f7 | ||
|
|
ae649a5b3f | ||
|
|
34675da6fa | ||
|
|
075b0292b5 | ||
|
|
0e9825f298 | ||
|
|
bf883794a4 | ||
|
|
3adaa2d602 | ||
|
|
60a312ace5 | ||
|
|
8c9a351c84 | ||
|
|
d28f512de4 | ||
|
|
376de685fb | ||
|
|
890515edfd | ||
|
|
79412ba2c6 | ||
|
|
03ebb6947b | ||
|
|
c53591f049 | ||
|
|
db9a32a4c0 | ||
|
|
2ae9bbb24e | ||
|
|
8dfe193461 | ||
|
|
27239ae14a | ||
|
|
80ff5e2d0a | ||
|
|
38240a68aa | ||
|
|
89710b406e | ||
|
|
f002870c6a | ||
|
|
b1a68759cf | ||
|
|
bf49a17efa | ||
|
|
6b0d1d77d3 | ||
|
|
db33b7d1dc | ||
|
|
76711d95ff | ||
|
|
17faa6c3b2 | ||
|
|
c1ac354c9b | ||
|
|
449503bb94 | ||
|
|
aad18e1ca4 | ||
|
|
fdc1123b18 | ||
|
|
0974b1e705 | ||
|
|
bc6e3e598f | ||
|
|
683f9d9a28 | ||
|
|
f9bf0883c6 | ||
|
|
ad8ba299c9 | ||
|
|
3ae64822e1 | ||
|
|
a49aaee9cf | ||
|
|
2d1f70eb79 | ||
|
|
4fc5f765de | ||
|
|
7efc2726d9 | ||
|
|
29a778697c | ||
|
|
90649e72bb | ||
|
|
1904ec7df4 | ||
|
|
96add30331 | ||
|
|
9c7148e2ad | ||
|
|
31c709be5c | ||
|
|
e750ee6d28 | ||
|
|
5b3f9ac1cd | ||
|
|
6c8dacfa0d | ||
|
|
feead30b0b |
@@ -2,7 +2,7 @@
|
|||||||
# It has the dependencies already installed so that codespace will boot up fast
|
# It has the dependencies already installed so that codespace will boot up fast
|
||||||
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
|
|
||||||
# Do the set up required for chatwoot app
|
# Do the set up required for chatwoot app
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
COPY . /workspace
|
COPY . /workspace
|
||||||
RUN yarn && gem install bundler && bundle install
|
RUN yarn && gem install bundler && bundle install
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
|
||||||
ARG VARIANT=ubuntu-20.04
|
ARG VARIANT
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
|
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
|
||||||
|
|
||||||
|
ARG NODE_VERSION
|
||||||
|
ARG RUBY_VERSION
|
||||||
|
ARG USER_UID
|
||||||
|
ARG USER_GID
|
||||||
|
|
||||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||||
ARG USER_UID=1000
|
|
||||||
ARG USER_GID=$USER_UID
|
|
||||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||||
groupmod --gid $USER_GID vscode \
|
groupmod --gid $USER_GID vscode \
|
||||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||||
@@ -27,10 +31,10 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|||||||
tmux \
|
tmux \
|
||||||
zsh \
|
zsh \
|
||||||
git-flow \
|
git-flow \
|
||||||
npm
|
npm \
|
||||||
|
libyaml-dev
|
||||||
|
|
||||||
# Install rbenv and ruby
|
# Install rbenv and ruby
|
||||||
ARG RUBY_VERSION="3.2.2"
|
|
||||||
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||||
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||||
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||||
@@ -46,7 +50,7 @@ RUN rbenv install $RUBY_VERSION && \
|
|||||||
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
||||||
&& gunzip overmind.gz \
|
&& gunzip overmind.gz \
|
||||||
&& sudo mv overmind /usr/local/bin \
|
&& sudo mv overmind /usr/local/bin \
|
||||||
&& chmod +x /usr/local/bin/overmind
|
&& chmod +x /usr/local/bin/overmind
|
||||||
|
|
||||||
|
|
||||||
# Install gh
|
# Install gh
|
||||||
@@ -56,7 +60,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | s
|
|||||||
&& sudo apt install gh
|
&& sudo apt install gh
|
||||||
|
|
||||||
|
|
||||||
# Do the set up required for chatwoot app
|
# Do the set up required for chatwoot app
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
COPY . /workspace
|
COPY . /workspace
|
||||||
|
|
||||||
@@ -65,8 +69,7 @@ COPY Gemfile Gemfile.lock ./
|
|||||||
RUN gem install bundler && bundle install
|
RUN gem install bundler && bundle install
|
||||||
|
|
||||||
# set up node js
|
# set up node js
|
||||||
RUN npm install npm@latest -g && \
|
RUN npm install n -g && \
|
||||||
npm install n -g && \
|
n $NODE_VERSION
|
||||||
n latest
|
|
||||||
RUN npm install --global yarn
|
RUN npm install --global yarn
|
||||||
RUN yarn
|
RUN yarn
|
||||||
|
|||||||
@@ -1,30 +1,41 @@
|
|||||||
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
|
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
|
||||||
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
|
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
|
||||||
#
|
#
|
||||||
|
|
||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
base:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/Dockerfile.base
|
||||||
|
args:
|
||||||
|
VARIANT: "ubuntu-22.04"
|
||||||
|
NODE_VERSION: "20.9.0"
|
||||||
|
RUBY_VERSION: "3.2.2"
|
||||||
|
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||||
|
USER_UID: "1000"
|
||||||
|
USER_GID: "1000"
|
||||||
|
image: base:latest
|
||||||
|
|
||||||
|
app:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: .devcontainer/Dockerfile
|
dockerfile: .devcontainer/Dockerfile
|
||||||
args:
|
args:
|
||||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
VARIANT: "ubuntu-22.04"
|
||||||
VARIANT: 3
|
NODE_VERSION: "20.9.0"
|
||||||
# [Choice] Install Node.js
|
RUBY_VERSION: "3.2.2"
|
||||||
INSTALL_NODE: "true"
|
|
||||||
NODE_VERSION: "lts/*"
|
|
||||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||||
USER_UID: 1000
|
USER_UID: "1000"
|
||||||
USER_GID: 1000
|
USER_GID: "1000"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
|
|
||||||
# Overrides default command so things don't shut down after the process ends.
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
|
|
||||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||||
network_mode: service:db
|
network_mode: service:db
|
||||||
|
|
||||||
@@ -49,8 +60,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: mailhog/mailhog
|
image: mailhog/mailhog
|
||||||
network_mode: service:db
|
network_mode: service:db
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
redis-data:
|
redis-data:
|
||||||
|
|
||||||
@@ -254,7 +254,6 @@ AZURE_APP_SECRET=
|
|||||||
# Sentiment analysis model file path
|
# Sentiment analysis model file path
|
||||||
SENTIMENT_FILE_PATH=
|
SENTIMENT_FILE_PATH=
|
||||||
|
|
||||||
|
|
||||||
# Housekeeping/Performance related configurations
|
# Housekeeping/Performance related configurations
|
||||||
# Set to true if you want to remove stale contact inboxes
|
# Set to true if you want to remove stale contact inboxes
|
||||||
# contact_inboxes with no conversation older than 90 days will be removed
|
# contact_inboxes with no conversation older than 90 days will be removed
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## All javascript files should be reviewed by pranav before merging
|
||||||
|
*.js @pranavrajs
|
||||||
|
*.vue @pranavrajs
|
||||||
|
|
||||||
|
|
||||||
|
## All enterprise related files should be reviewed by sojan before merging
|
||||||
|
/enterprise/* @sojan-official
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check for log lines and calculate percentage
|
- name: Check for log lines and calculate percentage
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# # This action will try to install and setup
|
# # This action will try to install and setup
|
||||||
# # chatwoot on an Ubuntu 20.04 machine using
|
# # chatwoot on an Ubuntu 20.04 machine using
|
||||||
# # the linux installer script.
|
# # the linux installer script.
|
||||||
# #
|
# #
|
||||||
# # This is set to run daily at midnight.
|
# # This is set to run daily at midnight.
|
||||||
# #
|
# #
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo ./install.sh --install < input
|
sudo ./install.sh --install < input
|
||||||
|
|
||||||
# disabling http verify for now as http
|
# disabling http verify for now as http
|
||||||
# access to port 3000 fails in gh action env
|
# access to port 3000 fails in gh action env
|
||||||
# - name: Verify
|
# - name: Verify
|
||||||
# if: always()
|
# if: always()
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
# curl http://localhost:3000/api
|
# curl http://localhost:3000/api
|
||||||
|
|
||||||
- name: Upload chatwoot setup log file as an artifact
|
- name: Upload chatwoot setup log file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: chatwoot-setup-log-file
|
name: chatwoot-setup-log-file
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
@@ -19,5 +19,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Build the Codespace Base Image
|
- name: Build the Codespace Base Image
|
||||||
run: |
|
run: |
|
||||||
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
|
docker-compose -f .devcontainer/docker-compose.yml build base
|
||||||
|
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
options: --entrypoint redis-server
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -76,11 +76,11 @@ jobs:
|
|||||||
- name: Run backend tests
|
- name: Run backend tests
|
||||||
run: |
|
run: |
|
||||||
bundle exec rspec --profile=10 --format documentation
|
bundle exec rspec --profile=10 --format documentation
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --openssl-legacy-provider
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
- name: Upload rails log folder
|
- name: Upload rails log folder
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: rails-log-folder
|
name: rails-log-folder
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
options: --entrypoint redis-server
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
--format documentation
|
--format documentation
|
||||||
|
|
||||||
- name: Upload rails log folder
|
- name: Upload rails log folder
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: rails-log-folder
|
name: rails-log-folder
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf enterprise
|
rm -rf enterprise
|
||||||
rm -rf spec/enterprise
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
- name: Run asset compile
|
- name: Run asset compile
|
||||||
run: bundle exec rake assets:precompile
|
run: bundle exec rake assets:precompile
|
||||||
env:
|
env:
|
||||||
|
|||||||
+11
-70
@@ -2,7 +2,6 @@ require:
|
|||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
inherit_from: .rubocop_todo.yml
|
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 150
|
Max: 150
|
||||||
@@ -12,7 +11,8 @@ Metrics/ClassLength:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Max: 19
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
@@ -50,6 +50,7 @@ Lint/OrAssignmentToConstant:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/redis/config.rb'
|
- 'lib/redis/config.rb'
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
|
Max: 30
|
||||||
Exclude:
|
Exclude:
|
||||||
- spec/**/*
|
- spec/**/*
|
||||||
- '**/routes.rb'
|
- '**/routes.rb'
|
||||||
@@ -102,84 +103,31 @@ RSpec/FactoryBot/SyntaxMethods:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
Naming/VariableNumber:
|
Naming/VariableNumber:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics/MethodLength:
|
Naming/MemoizedInstanceVariableName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
- 'app/models/message.rb'
|
||||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
|
||||||
Rails/CreateTableWithTimestamps:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
|
||||||
Style/GuardClause:
|
Style/GuardClause:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/builders/account_builder.rb'
|
- 'app/builders/account_builder.rb'
|
||||||
- 'app/models/attachment.rb'
|
- 'app/models/attachment.rb'
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
|
Max: 26
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/concerns/auth_helper.rb'
|
- 'app/controllers/concerns/auth_helper.rb'
|
||||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
|
||||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
|
||||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
|
||||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
|
||||||
Metrics/CyclomaticComplexity:
|
|
||||||
Max: 7
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
|
||||||
Rails/ReversibleMigration:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
|
||||||
- 'db/migrate/20161025070645_remchannel.rb'
|
|
||||||
- 'db/migrate/20161025070645_remchannel.rb'
|
|
||||||
- 'db/migrate/20161110102609_removeinboxid.rb'
|
|
||||||
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
|
|
||||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
|
||||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
|
||||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
|
||||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
|
||||||
Rails/BulkChangeTable:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
|
||||||
- 'db/migrate/20200121190901_create_account_users.rb'
|
|
||||||
- 'db/migrate/20170211092540_notnullableusers.rb'
|
|
||||||
- 'db/migrate/20170403095203_contactadder.rb'
|
|
||||||
- 'db/migrate/20170406104018_add_default_status_conv.rb'
|
|
||||||
- 'db/migrate/20170511134418_latlong.rb'
|
|
||||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
|
||||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
|
||||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
|
||||||
Rails/UniqueValidationWithoutIndex:
|
Rails/UniqueValidationWithoutIndex:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/channel/twitter_profile.rb'
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
- 'app/models/webhook.rb'
|
- 'app/models/webhook.rb'
|
||||||
- 'app/models/contact.rb'
|
- 'app/models/contact.rb'
|
||||||
- 'app/models/integrations/hook.rb'
|
- 'app/models/integrations/hook.rb'
|
||||||
|
- 'app/models/canned_response.rb'
|
||||||
|
- 'app/models/telegram_bot.rb'
|
||||||
Rails/RenderInline:
|
Rails/RenderInline:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/swagger_controller.rb'
|
- 'app/controllers/swagger_controller.rb'
|
||||||
Performance/CollectionLiteralInLoop:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
|
||||||
Rails/ThreeStateBooleanColumn:
|
Rails/ThreeStateBooleanColumn:
|
||||||
Exclude:
|
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'
|
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||||
RSpec/IndexedLet:
|
RSpec/IndexedLet:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -187,6 +135,8 @@ RSpec/NamedSubject:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
|
RSpec/MultipleExpectations:
|
||||||
|
Max: 7
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 14
|
Max: 14
|
||||||
|
|
||||||
@@ -203,13 +153,4 @@ AllCops:
|
|||||||
- 'config/environments/**/*'
|
- 'config/environments/**/*'
|
||||||
- 'tmp/**/*'
|
- 'tmp/**/*'
|
||||||
- 'storage/**/*'
|
- 'storage/**/*'
|
||||||
- 'db/migrate/20200225162150_init_schema.rb'
|
- 'db/migrate/20230426130150_init_schema.rb'
|
||||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
|
||||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
|
||||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
|
||||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
|
||||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
|
||||||
- db/migrate/20200503151130_add_account_feature_flag.rb
|
|
||||||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
|
||||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
|
||||||
- db/migrate/20220809104508_revert_cascading_indexes.rb
|
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
# This configuration was generated by
|
|
||||||
# `rubocop --auto-gen-config`
|
|
||||||
# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0.
|
|
||||||
# The point is for the user to remove these configuration records
|
|
||||||
# one by one as the offenses are removed from the code base.
|
|
||||||
# Note that changes in the inspected code, or installation of new
|
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
Lint/DuplicateMethods:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/reports_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
Lint/RescueException:
|
|
||||||
Exclude:
|
|
||||||
- 'app/builders/messages/message_builder.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
Lint/ShadowingOuterLocalVariable:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/reports_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 3
|
|
||||||
# Configuration parameters: AllowKeywordBlockArguments.
|
|
||||||
Lint/UnderscorePrefixedVariableName:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/account.rb'
|
|
||||||
- 'deploy/before_symlink.rb'
|
|
||||||
|
|
||||||
# Offense count: 18
|
|
||||||
Lint/UselessAssignment:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/facebook_indicators_controller.rb'
|
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
|
||||||
- 'app/listeners/reporting_listener.rb'
|
|
||||||
- 'app/models/channel/facebook_page.rb'
|
|
||||||
- 'app/models/facebook_page.rb'
|
|
||||||
|
|
||||||
# Offense count: 14
|
|
||||||
Metrics/AbcSize:
|
|
||||||
Max: 26
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: CountComments, ExcludedMethods.
|
|
||||||
# ExcludedMethods: refine
|
|
||||||
Metrics/BlockLength:
|
|
||||||
Max: 30
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
Metrics/CyclomaticComplexity:
|
|
||||||
Max: 7
|
|
||||||
|
|
||||||
# Offense count: 10
|
|
||||||
# Configuration parameters: CountComments, ExcludedMethods.
|
|
||||||
Metrics/MethodLength:
|
|
||||||
Max: 19
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
Metrics/PerceivedComplexity:
|
|
||||||
Max: 8
|
|
||||||
|
|
||||||
# Offense count: 6
|
|
||||||
Naming/AccessorMethodName:
|
|
||||||
Exclude:
|
|
||||||
- 'app/builders/report_builder.rb'
|
|
||||||
- 'app/controllers/api/v1/accounts_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 9
|
|
||||||
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
|
||||||
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
|
||||||
Naming/MemoizedInstanceVariableName:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/base_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/webhooks_controller.rb'
|
|
||||||
- 'app/controllers/application_controller.rb'
|
|
||||||
- 'app/models/message.rb'
|
|
||||||
- 'lib/integrations/widget/outgoing_message_builder.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: MaxKeyValuePairs.
|
|
||||||
Performance/RedundantMerge:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
- 'app/models/message.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Performance/StringReplacement:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/events/base.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
# Configuration parameters: Prefixes.
|
|
||||||
# Prefixes: when, with, without
|
|
||||||
RSpec/ContextWording:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/contact_spec.rb'
|
|
||||||
- 'spec/models/user_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
RSpec/DescribeClass:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
RSpec/DescribeSymbol:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: AllowConsecutiveOneLiners.
|
|
||||||
RSpec/EmptyLineAfterExample:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/user_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: Max.
|
|
||||||
RSpec/ExampleLength:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/conversation_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: single_line_only, single_statement_only, disallow
|
|
||||||
RSpec/ImplicitSubject:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/user_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 7
|
|
||||||
# Configuration parameters: AggregateFailuresByDefault.
|
|
||||||
RSpec/MultipleExpectations:
|
|
||||||
Max: 7
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: not_to, to_not
|
|
||||||
RSpec/NotToNot:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
|
||||||
RSpec/VerifiedDoubles:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/conversation_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Rails/ActiveRecordAliases:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/agents_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/canned_responses_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/contacts_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Rails/BelongsTo:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/message.rb'
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
|
|
||||||
# Offense count: 6
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/models/**/*.rb
|
|
||||||
Rails/EnumHash:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/attachment.rb'
|
|
||||||
- 'app/models/conversation.rb'
|
|
||||||
- 'app/models/message.rb'
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/models/**/*.rb
|
|
||||||
Rails/HasManyOrHasOneDependent:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/models/**/*.rb
|
|
||||||
Rails/InverseOf:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/controllers/**/*.rb
|
|
||||||
Rails/LexicallyScopedActionFilter:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/home_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
|
|
||||||
Rails/Output:
|
|
||||||
Exclude:
|
|
||||||
- 'app/bot/bot.rb'
|
|
||||||
- 'app/builders/account_builder.rb'
|
|
||||||
|
|
||||||
# Offense count: 7
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: strict, flexible
|
|
||||||
Rails/TimeZone:
|
|
||||||
Exclude:
|
|
||||||
- 'app/builders/report_builder.rb'
|
|
||||||
- 'lib/reports/update_account_identity.rb'
|
|
||||||
- 'lib/reports/update_agent_identity.rb'
|
|
||||||
- 'lib/reports/update_identity.rb'
|
|
||||||
- 'spec/models/conversation_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 8
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/models/**/*.rb
|
|
||||||
Rails/Validation:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/canned_response.rb'
|
|
||||||
- 'app/models/channel/facebook_page.rb'
|
|
||||||
- 'app/models/facebook_page.rb'
|
|
||||||
- 'app/models/telegram_bot.rb'
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
|
|
||||||
# Offense count: 15
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
|
||||||
# SupportedStyles: nested, compact
|
|
||||||
Style/ClassAndModuleChildren:
|
|
||||||
Exclude:
|
|
||||||
- 'app/builders/messages/message_builder.rb'
|
|
||||||
- 'app/controllers/api/v1/inbox_members_controller.rb'
|
|
||||||
- 'app/models/channel/facebook_page.rb'
|
|
||||||
- 'app/models/channel/web_widget.rb'
|
|
||||||
- 'app/presenters/conversations/event_data_presenter.rb'
|
|
||||||
- 'app/services/facebook/send_reply_service.rb'
|
|
||||||
- 'lib/integrations/facebook/delivery_status.rb'
|
|
||||||
- 'lib/integrations/facebook/message_creator.rb'
|
|
||||||
- 'lib/integrations/facebook/message_parser.rb'
|
|
||||||
- 'lib/integrations/widget/incoming_message_builder.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
Style/CommentedKeyword:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/conversations/assignments_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/conversations/labels_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/labels_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: AllowIfModifier.
|
|
||||||
Style/IfInsideElse:
|
|
||||||
Exclude:
|
|
||||||
- 'app/finders/conversation_finder.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
Style/MixinUsage:
|
|
||||||
Exclude:
|
|
||||||
- 'app/bot/bot.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
|
||||||
# SupportedStyles: predicate, comparison
|
|
||||||
Style/NumericPredicate:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/**/*'
|
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: implicit, explicit
|
|
||||||
Style/RescueStandardError:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/channel/facebook_page.rb'
|
|
||||||
@@ -39,6 +39,8 @@ gem 'rack-attack', '>= 6.7.0'
|
|||||||
gem 'down'
|
gem 'down'
|
||||||
# authentication type to fetch and send mail over oauth2.0
|
# authentication type to fetch and send mail over oauth2.0
|
||||||
gem 'gmail_xoauth'
|
gem 'gmail_xoauth'
|
||||||
|
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
|
||||||
|
gem 'net-smtp', '~> 0.3.4'
|
||||||
# Prevent CSV injection
|
# Prevent CSV injection
|
||||||
gem 'csv-safe'
|
gem 'csv-safe'
|
||||||
|
|
||||||
@@ -74,9 +76,9 @@ gem 'devise_token_auth'
|
|||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
# super admin
|
# super admin
|
||||||
gem 'administrate', '>= 0.19.0'
|
gem 'administrate', '>= 0.20.1'
|
||||||
gem 'administrate-field-active_storage'
|
gem 'administrate-field-active_storage', '>= 1.0.1'
|
||||||
gem 'administrate-field-belongs_to_search'
|
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
|
||||||
|
|
||||||
##--- gems for pubsub service ---##
|
##--- gems for pubsub service ---##
|
||||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||||
@@ -109,14 +111,14 @@ gem 'elastic-apm', require: false
|
|||||||
gem 'newrelic_rpm', require: false
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||||
gem 'scout_apm', require: false
|
gem 'scout_apm', require: false
|
||||||
gem 'sentry-rails', '>= 5.13.0', require: false
|
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||||
gem 'sentry-ruby', require: false
|
gem 'sentry-ruby', require: false
|
||||||
gem 'sentry-sidekiq', '>= 5.13.0', require: false
|
gem 'sentry-sidekiq', '>= 5.14.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '>= 7.1.3'
|
gem 'sidekiq', '>= 7.2.1'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '>= 1.11.0'
|
gem 'sidekiq-cron', '>= 1.12.0'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@@ -162,7 +164,7 @@ gem 'omniauth-oauth2'
|
|||||||
gem 'audited', '~> 5.4', '>= 5.4.1'
|
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||||
|
|
||||||
# need for google auth
|
# need for google auth
|
||||||
gem 'omniauth'
|
gem 'omniauth', '>= 2.1.2'
|
||||||
gem 'omniauth-google-oauth2'
|
gem 'omniauth-google-oauth2'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
|
|
||||||
@@ -198,7 +200,7 @@ group :development do
|
|||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
|
||||||
# profiling
|
# profiling
|
||||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
gem 'rack-mini-profiler', '>= 3.2.0', require: false
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
# Should install the associated chrome extension to view query logs
|
# Should install the associated chrome extension to view query logs
|
||||||
gem 'meta_request'
|
gem 'meta_request'
|
||||||
@@ -224,7 +226,7 @@ group :development, :test do
|
|||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
gem 'climate_control'
|
gem 'climate_control'
|
||||||
gem 'debug', '~> 1.8'
|
gem 'debug', '~> 1.8'
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails', '>= 6.4.3'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
|
|||||||
+64
-63
@@ -105,21 +105,21 @@ GEM
|
|||||||
activerecord (>= 6.0, < 7.1)
|
activerecord (>= 6.0, < 7.1)
|
||||||
addressable (2.8.4)
|
addressable (2.8.4)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
administrate (0.19.0)
|
administrate (0.20.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 6.0, < 8.0)
|
||||||
actionview (>= 5.0)
|
actionview (>= 6.0, < 8.0)
|
||||||
activerecord (>= 5.0)
|
activerecord (>= 6.0, < 8.0)
|
||||||
jquery-rails (>= 4.0)
|
jquery-rails (~> 4.6.0)
|
||||||
kaminari (>= 1.0)
|
kaminari (~> 1.2.2)
|
||||||
sassc-rails (~> 2.1)
|
sassc-rails (~> 2.1)
|
||||||
selectize-rails (~> 0.6)
|
selectize-rails (~> 0.6)
|
||||||
administrate-field-active_storage (0.4.2)
|
administrate-field-active_storage (1.0.1)
|
||||||
administrate (>= 0.2.2)
|
administrate (>= 0.2.2)
|
||||||
rails (>= 7.0)
|
rails (>= 7.0)
|
||||||
administrate-field-belongs_to_search (0.8.0)
|
administrate-field-belongs_to_search (0.9.0)
|
||||||
administrate (>= 0.3, < 1.0)
|
administrate (>= 0.3, < 1.0)
|
||||||
jbuilder (~> 2)
|
jbuilder (~> 2)
|
||||||
rails (>= 4.2, < 7.1)
|
rails (>= 4.2, < 7.2)
|
||||||
selectize-rails (~> 0.6)
|
selectize-rails (~> 0.6)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
@@ -169,7 +169,7 @@ GEM
|
|||||||
climate_control (1.2.0)
|
climate_control (1.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.10)
|
commonmarker (0.23.10)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
@@ -183,7 +183,7 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
ddtrace (1.11.1)
|
ddtrace (1.11.1)
|
||||||
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
||||||
libdatadog (~> 2.0.0.1.0)
|
libdatadog (~> 2.0.0.1.0)
|
||||||
@@ -230,10 +230,10 @@ GEM
|
|||||||
facebook-messenger (2.0.1)
|
facebook-messenger (2.0.1)
|
||||||
httparty (~> 0.13, >= 0.13.7)
|
httparty (~> 0.13, >= 0.13.7)
|
||||||
rack (>= 1.4.5)
|
rack (>= 1.4.5)
|
||||||
factory_bot (6.2.1)
|
factory_bot (6.4.5)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
@@ -256,7 +256,7 @@ GEM
|
|||||||
fcm (1.0.8)
|
fcm (1.0.8)
|
||||||
faraday (>= 1.0.0, < 3.0)
|
faraday (>= 1.0.0, < 3.0)
|
||||||
googleauth (~> 1)
|
googleauth (~> 1)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
@@ -277,7 +277,7 @@ GEM
|
|||||||
gli (2.21.1)
|
gli (2.21.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.3)
|
||||||
oauth (>= 0.3.6)
|
oauth (>= 0.3.6)
|
||||||
google-apis-core (0.11.0)
|
google-apis-core (0.11.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
@@ -316,16 +316,16 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.6.0)
|
google-cloud-translate-v3 (0.6.0)
|
||||||
gapic-common (>= 0.17.1, < 2.a)
|
gapic-common (>= 0.17.1, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-protobuf (3.22.3)
|
google-protobuf (3.25.2)
|
||||||
google-protobuf (3.22.3-arm64-darwin)
|
google-protobuf (3.25.2-arm64-darwin)
|
||||||
google-protobuf (3.22.3-x86_64-darwin)
|
google-protobuf (3.25.2-x86_64-darwin)
|
||||||
google-protobuf (3.22.3-x86_64-linux)
|
google-protobuf (3.25.2-x86_64-linux)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.6.0)
|
googleapis-common-protos-types (1.11.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.18)
|
||||||
googleauth (1.5.2)
|
googleauth (1.5.2)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
@@ -335,13 +335,13 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.54.0)
|
grpc (1.54.3)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.21)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.0-x86_64-darwin)
|
grpc (1.54.3-x86_64-darwin)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.21)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.0-x86_64-linux)
|
grpc (1.54.3-x86_64-linux)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.21)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
@@ -440,7 +440,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.4)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -461,7 +461,7 @@ GEM
|
|||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.5)
|
||||||
minitest (5.20.0)
|
minitest (5.21.2)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
msgpack (1.7.0)
|
msgpack (1.7.0)
|
||||||
@@ -472,14 +472,14 @@ GEM
|
|||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.3.4)
|
||||||
net-protocol
|
net-protocol
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic-sidekiq-metrics (1.6.2)
|
newrelic-sidekiq-metrics (1.6.2)
|
||||||
@@ -487,15 +487,15 @@ GEM
|
|||||||
sidekiq
|
sidekiq
|
||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.0)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.16.2)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
nokogiri (1.16.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-x86_64-darwin)
|
nokogiri (1.16.2-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-x86_64-linux)
|
nokogiri (1.16.2-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
numo-narray (0.9.2.1)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
@@ -511,7 +511,7 @@ GEM
|
|||||||
rack (>= 1.2, < 4)
|
rack (>= 1.2, < 4)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
version_gem (~> 1.1)
|
version_gem (~> 1.1)
|
||||||
omniauth (2.1.1)
|
omniauth (2.1.2)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
@@ -553,7 +553,7 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.1)
|
||||||
puma (6.3.1)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@@ -566,10 +566,10 @@ GEM
|
|||||||
rack (< 4)
|
rack (< 4)
|
||||||
rack-cors (2.0.1)
|
rack-cors (2.0.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.1.1)
|
rack-mini-profiler (3.2.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-protection (3.0.6)
|
rack-protection (3.1.0)
|
||||||
rack
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
@@ -610,7 +610,7 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (5.0.6)
|
redis (5.0.6)
|
||||||
redis-client (>= 0.9.0)
|
redis-client (>= 0.9.0)
|
||||||
redis-client (0.18.0)
|
redis-client (0.19.1)
|
||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
@@ -709,23 +709,23 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sentry-rails (5.13.0)
|
sentry-rails (5.14.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.13.0)
|
sentry-ruby (~> 5.14.0)
|
||||||
sentry-ruby (5.13.0)
|
sentry-ruby (5.14.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.13.0)
|
sentry-sidekiq (5.14.0)
|
||||||
sentry-ruby (~> 5.13.0)
|
sentry-ruby (~> 5.14.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (5.3.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.2.0)
|
sidekiq (7.2.1)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.19.0)
|
||||||
sidekiq-cron (1.11.0)
|
sidekiq-cron (1.12.0)
|
||||||
fugit (~> 1.8)
|
fugit (~> 1.8)
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
@@ -752,7 +752,7 @@ GEM
|
|||||||
spring-watcher-listen (2.1.0)
|
spring-watcher-listen (2.1.0)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
spring (>= 4)
|
spring (>= 4)
|
||||||
sprockets (4.2.0)
|
sprockets (4.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (>= 2.2.4, < 4)
|
rack (>= 2.2.4, < 4)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.4.2)
|
||||||
@@ -766,11 +766,11 @@ GEM
|
|||||||
telephone_number (1.4.20)
|
telephone_number (1.4.20)
|
||||||
test-prof (1.2.1)
|
test-prof (1.2.1)
|
||||||
thor (1.3.0)
|
thor (1.3.0)
|
||||||
tilt (2.2.0)
|
tilt (2.3.0)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
i18n
|
i18n
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
twilio-ruby (5.77.0)
|
twilio-ruby (5.77.0)
|
||||||
faraday (>= 0.9, < 3.0)
|
faraday (>= 0.9, < 3.0)
|
||||||
@@ -794,7 +794,7 @@ GEM
|
|||||||
valid_email2 (4.0.6)
|
valid_email2 (4.0.6)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
version_gem (1.1.2)
|
version_gem (1.1.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
@@ -840,9 +840,9 @@ DEPENDENCIES
|
|||||||
active_record_query_trace
|
active_record_query_trace
|
||||||
activerecord-import
|
activerecord-import
|
||||||
acts-as-taggable-on
|
acts-as-taggable-on
|
||||||
administrate (>= 0.19.0)
|
administrate (>= 0.20.1)
|
||||||
administrate-field-active_storage
|
administrate-field-active_storage (>= 1.0.1)
|
||||||
administrate-field-belongs_to_search
|
administrate-field-belongs_to_search (>= 0.9.0)
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.4, >= 5.4.1)
|
audited (~> 5.4, >= 5.4.1)
|
||||||
@@ -870,7 +870,7 @@ DEPENDENCIES
|
|||||||
elastic-apm
|
elastic-apm
|
||||||
email_reply_trimmer
|
email_reply_trimmer
|
||||||
facebook-messenger
|
facebook-messenger
|
||||||
factory_bot_rails
|
factory_bot_rails (>= 6.4.3)
|
||||||
faker
|
faker
|
||||||
fcm
|
fcm
|
||||||
flag_shih_tzu
|
flag_shih_tzu
|
||||||
@@ -903,9 +903,10 @@ DEPENDENCIES
|
|||||||
meta_request
|
meta_request
|
||||||
mock_redis
|
mock_redis
|
||||||
neighbor
|
neighbor
|
||||||
|
net-smtp (~> 0.3.4)
|
||||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
omniauth
|
omniauth (>= 2.1.2)
|
||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
@@ -918,7 +919,7 @@ DEPENDENCIES
|
|||||||
pundit
|
pundit
|
||||||
rack-attack (>= 6.7.0)
|
rack-attack (>= 6.7.0)
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler (>= 3.1.1)
|
rack-mini-profiler (>= 3.2.0)
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 7.0.8.0)
|
rails (~> 7.0.8.0)
|
||||||
redis
|
redis
|
||||||
@@ -935,12 +936,12 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.13.0)
|
sentry-rails (>= 5.14.0)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.13.0)
|
sentry-sidekiq (>= 5.14.0)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (>= 7.1.3)
|
sidekiq (>= 7.2.1)
|
||||||
sidekiq-cron (>= 1.11.0)
|
sidekiq-cron (>= 1.12.0)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client (~> 2.2.0)
|
slack-ruby-client (~> 2.2.0)
|
||||||
spring
|
spring
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
Copyright (c) 2017-2024 Chatwoot Inc.
|
||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
|
|||||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||||
|
|
||||||
|
|
||||||
*Chatwoot* © 2017-2023, Chatwoot Inc - Released under the MIT License.
|
*Chatwoot* © 2017-2024, Chatwoot Inc - Released under the MIT License.
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.1.0
|
3.3.1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.6.0
|
2.7.0
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class ContactIdentifyAction
|
|||||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||||
@contact.discard_invalid_attrs if discard_invalid_attrs
|
@contact.discard_invalid_attrs if discard_invalid_attrs
|
||||||
@contact.save!
|
@contact.save!
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached?
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_contact(base_contact, merge_contact)
|
def merge_contact(base_contact, merge_contact)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class ContactMergeAction
|
|||||||
merge_conversations
|
merge_conversations
|
||||||
merge_messages
|
merge_messages
|
||||||
merge_contact_inboxes
|
merge_contact_inboxes
|
||||||
|
merge_contact_notes
|
||||||
merge_and_remove_mergee_contact
|
merge_and_remove_mergee_contact
|
||||||
end
|
end
|
||||||
@base_contact
|
@base_contact
|
||||||
@@ -33,6 +34,10 @@ class ContactMergeAction
|
|||||||
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_contact_notes
|
||||||
|
Note.where(contact_id: @mergee_contact.id, account_id: @mergee_contact.account_id).update(contact_id: @base_contact.id)
|
||||||
|
end
|
||||||
|
|
||||||
def merge_messages
|
def merge_messages
|
||||||
Message.where(sender: @mergee_contact).update(sender: @base_contact)
|
Message.where(sender: @mergee_contact).update(sender: @base_contact)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
@import 'components/flashes';
|
@import 'components/flashes';
|
||||||
@import 'components/form-actions';
|
@import 'components/form-actions';
|
||||||
@import 'components/main-content';
|
@import 'components/main-content';
|
||||||
@import 'components/navigation';
|
|
||||||
@import 'components/pagination';
|
@import 'components/pagination';
|
||||||
@import 'components/search';
|
@import 'components/search';
|
||||||
@import 'components/reports';
|
@import 'components/reports';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
html {
|
html {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 10px;
|
font-size: 16px;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
.attribute-data {
|
.attribute-data {
|
||||||
float: left;
|
float: left;
|
||||||
margin-bottom: $base-spacing;
|
margin-bottom: $base-spacing;
|
||||||
margin-left: 2rem;
|
margin-left: 1.25rem;
|
||||||
width: calc(84% - 1rem);
|
width: calc(84% - 0.625rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribute--nested {
|
.attribute--nested {
|
||||||
|
|||||||
@@ -9,22 +9,22 @@
|
|||||||
|
|
||||||
.field-unit__label {
|
.field-unit__label {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 1rem;
|
margin-left: 0.625rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: calc(15% - 1rem);
|
width: calc(15% - 0.625rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-unit__field {
|
.field-unit__field {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 2rem;
|
margin-left: 1.25rem;
|
||||||
max-width: 50rem;
|
max-width: 31.15rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-unit--nested {
|
.field-unit--nested {
|
||||||
border: $base-border;
|
border: $base-border;
|
||||||
margin-left: 7.5%;
|
margin-left: 7.5%;
|
||||||
max-width: 60rem;
|
max-width: 37.5rem;
|
||||||
padding: $small-spacing;
|
padding: $small-spacing;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.form-actions {
|
.form-actions {
|
||||||
margin-left: calc(15% + 2rem);
|
margin-left: calc(15% + 1.25rem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
table {
|
table {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-top: $space-two;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__header {
|
.main-content__header {
|
||||||
@@ -20,7 +24,7 @@
|
|||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border-bottom: 1px solid $color-border;
|
border-bottom: 1px solid $color-border;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 5.6rem;
|
min-height: 3.5rem;
|
||||||
padding: $space-small $space-normal;
|
padding: $space-small $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
.logo-brand {
|
|
||||||
margin-bottom: $space-normal;
|
|
||||||
padding: $space-normal $space-smaller $space-small;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-bottom: $space-smaller;
|
|
||||||
max-height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation {
|
|
||||||
background: $white;
|
|
||||||
border-right: 1px solid $color-border;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: $font-size-default;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: $space-normal;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 21rem;
|
|
||||||
z-index: 1023;
|
|
||||||
|
|
||||||
li {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $color-gray;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
min-width: $space-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
margin: $space-slab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation__link {
|
|
||||||
background-color: transparent;
|
|
||||||
color: $color-gray;
|
|
||||||
display: block;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: $space-smaller;
|
|
||||||
padding: $space-small;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $blue;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&.navigation__link--active {
|
|
||||||
background-color: $color-background;
|
|
||||||
border-radius: $base-border-radius;
|
|
||||||
color: $blue;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout {
|
|
||||||
bottom: $space-normal;
|
|
||||||
left: $space-normal;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-version {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
padding-top: $space-smaller;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.search {
|
.search {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 2rem;
|
margin-right: 1.25rem;
|
||||||
max-width: 44rem;
|
max-width: 27.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
margin-right: 4px;
|
margin-right: 2px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Typography
|
// Typography
|
||||||
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif !default;
|
sans-serif !default;
|
||||||
$heading-font-family: $base-font-family !default;
|
$heading-font-family: $base-font-family !default;
|
||||||
|
|
||||||
$base-font-size: 14px !default;
|
$base-font-size: 16px !default;
|
||||||
|
|
||||||
$base-line-height: 1.5 !default;
|
$base-line-height: 1.5 !default;
|
||||||
$heading-line-height: 1.2 !default;
|
$heading-line-height: 1.2 !default;
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
// Font sizes
|
// Font sizes
|
||||||
$font-size-nano: 0.8rem;
|
$font-size-nano: 0.5rem;
|
||||||
$font-size-micro: 1.0rem;
|
$font-size-micro: 0.675rem;
|
||||||
$font-size-mini: 1.2rem;
|
$font-size-mini: 0.75rem;
|
||||||
$font-size-small: 1.4rem;
|
$font-size-small: 0.875rem;
|
||||||
$font-size-default: 1.6rem;
|
$font-size-default: 1rem;
|
||||||
$font-size-medium: 1.8rem;
|
$font-size-medium: 1.125rem;
|
||||||
$font-size-large: 2.2rem;
|
$font-size-large: 1.375rem;
|
||||||
$font-size-big: 2.4rem;
|
$font-size-big: 1.5rem;
|
||||||
$font-size-bigger: 3.0rem;
|
$font-size-bigger: 1.75rem;
|
||||||
$font-size-mega: 3.4rem;
|
$font-size-mega: 2.125rem;
|
||||||
$font-size-giga: 4.0rem;
|
$font-size-giga: 2.5rem;
|
||||||
|
|
||||||
// spaces
|
// spaces
|
||||||
$zero: 0;
|
$zero: 0;
|
||||||
$space-micro: 0.2rem;
|
$space-micro: 0.125rem;
|
||||||
$space-smaller: 0.4rem;
|
$space-smaller: 0.25rem;
|
||||||
$space-small: 0.8rem;
|
$space-small: 0.5rem;
|
||||||
$space-one: 1rem;
|
$space-one: 0.675rem;
|
||||||
$space-slab: 1.2rem;
|
$space-slab: 0.75rem;
|
||||||
$space-normal: 1.6rem;
|
$space-normal: 1rem;
|
||||||
$space-two: 2.0rem;
|
$space-two: 1.25rem;
|
||||||
$space-medium: 2.4rem;
|
$space-medium: 1.5rem;
|
||||||
$space-large: 3.2rem;
|
$space-large: 2rem;
|
||||||
$space-larger: 4.8rem;
|
$space-larger: 3rem;
|
||||||
$space-jumbo: 6.4rem;
|
$space-jumbo: 4rem;
|
||||||
$space-mega: 10.0rem;
|
$space-mega: 6.25rem;
|
||||||
|
|
||||||
// font-weight
|
// font-weight
|
||||||
$font-weight-feather: 100;
|
$font-weight-feather: 100;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class AccountBuilder
|
class AccountBuilder
|
||||||
include CustomExceptions::Account
|
include CustomExceptions::Account
|
||||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
if @user.nil?
|
if @user.nil?
|
||||||
@@ -15,12 +15,22 @@ class AccountBuilder
|
|||||||
end
|
end
|
||||||
[@user, @account]
|
[@user, @account]
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
puts e.inspect
|
Rails.logger.debug e.inspect
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def user_full_name
|
||||||
|
# the empty string ensures that not-null constraint is not violated
|
||||||
|
@user_full_name || ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_name
|
||||||
|
# the empty string ensures that not-null constraint is not violated
|
||||||
|
@account_name || ''
|
||||||
|
end
|
||||||
|
|
||||||
def validate_email
|
def validate_email
|
||||||
address = ValidEmail2::Address.new(@email)
|
address = ValidEmail2::Address.new(@email)
|
||||||
if address.valid? # && !address.disposable?
|
if address.valid? # && !address.disposable?
|
||||||
@@ -39,7 +49,7 @@ class AccountBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_account
|
def create_account
|
||||||
@account = Account.create!(name: @account_name, locale: I18n.locale)
|
@account = Account.create!(name: account_name, locale: I18n.locale)
|
||||||
Current.account = @account
|
Current.account = @account
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,7 +74,7 @@ class AccountBuilder
|
|||||||
@user = User.new(email: @email,
|
@user = User.new(email: @email,
|
||||||
password: user_password,
|
password: user_password,
|
||||||
password_confirmation: user_password,
|
password_confirmation: user_password,
|
||||||
name: @user_full_name)
|
name: user_full_name)
|
||||||
@user.type = 'SuperAdmin' if @super_admin
|
@user.type = 'SuperAdmin' if @super_admin
|
||||||
@user.confirm if @confirmed
|
@user.confirm if @confirmed
|
||||||
@user.save!
|
@user.save!
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# The AgentBuilder class is responsible for creating a new agent.
|
||||||
|
# It initializes with necessary attributes and provides a perform method
|
||||||
|
# to create a user and account user in a transaction.
|
||||||
|
class AgentBuilder
|
||||||
|
# Initializes an AgentBuilder with necessary attributes.
|
||||||
|
# @param email [String] the email of the user.
|
||||||
|
# @param name [String] the name of the user.
|
||||||
|
# @param role [String] the role of the user, defaults to 'agent' if not provided.
|
||||||
|
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
|
||||||
|
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
|
||||||
|
# @param auto_offline [Boolean] the auto offline status of the user.
|
||||||
|
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
|
||||||
|
|
||||||
|
# Creates a user and account user in a transaction.
|
||||||
|
# @return [User] the created user.
|
||||||
|
def perform
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@user = find_or_create_user
|
||||||
|
send_confirmation_if_required
|
||||||
|
create_account_user
|
||||||
|
end
|
||||||
|
@user
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Finds a user by email or creates a new one with a temporary password.
|
||||||
|
# @return [User] the found or created user.
|
||||||
|
def find_or_create_user
|
||||||
|
user = User.find_by(email: email)
|
||||||
|
return user if user
|
||||||
|
|
||||||
|
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||||
|
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends confirmation instructions if the user is persisted and not confirmed.
|
||||||
|
def send_confirmation_if_required
|
||||||
|
@user.send_confirmation_instructions if user_needs_confirmation?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the user needs confirmation.
|
||||||
|
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
|
||||||
|
def user_needs_confirmation?
|
||||||
|
@user.persisted? && !@user.confirmed?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Creates an account user linking the user to the current account.
|
||||||
|
def create_account_user
|
||||||
|
AccountUser.create!({
|
||||||
|
account_id: account.id,
|
||||||
|
user_id: @user.id,
|
||||||
|
inviter_id: inviter.id
|
||||||
|
}.merge({
|
||||||
|
role: role,
|
||||||
|
availability: availability,
|
||||||
|
auto_offline: auto_offline
|
||||||
|
}.compact))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,7 +25,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
build_contact_inbox
|
build_contact_inbox
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
@@ -108,11 +110,15 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
def contact_params
|
def contact_params
|
||||||
begin
|
begin
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
result = k.get_object(@sender_id) || {}
|
result = k.get_object(@sender_id) || {}
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
@@ -130,4 +136,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
end
|
end
|
||||||
process_contact_params_result(result)
|
process_contact_params_result(result)
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@@ -46,6 +48,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
@outgoing_echo ? recipient_id : sender_id
|
@outgoing_echo ? recipient_id : sender_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_is_unsupported?
|
||||||
|
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
|
||||||
|
end
|
||||||
|
|
||||||
def sender_id
|
def sender_id
|
||||||
@messaging[:sender][:id]
|
@messaging[:sender][:id]
|
||||||
end
|
end
|
||||||
@@ -116,7 +122,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def message_params
|
def message_params
|
||||||
{
|
params = {
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
message_type: message_type,
|
message_type: message_type,
|
||||||
@@ -127,6 +133,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
in_reply_to_external_id: message_reply_attributes
|
in_reply_to_external_id: message_reply_attributes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
|
||||||
|
params
|
||||||
end
|
end
|
||||||
|
|
||||||
def already_sent_from_chatwoot?
|
def already_sent_from_chatwoot?
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ class Messages::MessageBuilder
|
|||||||
content_type: @params[:content_type],
|
content_type: @params[:content_type],
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id],
|
||||||
|
source_id: @params[:source_id]
|
||||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
class NotificationBuilder
|
class NotificationBuilder
|
||||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return unless user_subscribed_to_notification?
|
|
||||||
|
|
||||||
build_notification
|
build_notification
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def secondary_actor
|
def current_user
|
||||||
Current.user
|
Current.user
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_subscribed_to_notification?
|
def user_subscribed_to_notification?
|
||||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||||
# added for the case where an assignee might be removed from the account but remains in conversation
|
# added for the case where an assignee might be removed from the account but remains in conversation
|
||||||
return if notification_setting.blank?
|
return false if notification_setting.blank?
|
||||||
|
|
||||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||||
@@ -25,11 +23,15 @@ class NotificationBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_notification
|
def build_notification
|
||||||
|
# Create conversation_creation notification only if user is subscribed to it
|
||||||
|
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
|
||||||
|
|
||||||
user.notifications.create!(
|
user.notifications.create!(
|
||||||
notification_type: notification_type,
|
notification_type: notification_type,
|
||||||
account: account,
|
account: account,
|
||||||
primary_actor: primary_actor,
|
primary_actor: primary_actor,
|
||||||
secondary_actor: secondary_actor
|
# secondary_actor is secondary_actor if present, else current_user
|
||||||
|
secondary_actor: secondary_actor || current_user
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ class V2::ReportBuilder
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def short_summary
|
||||||
|
{
|
||||||
|
conversations_count: conversations.count,
|
||||||
|
avg_first_response_time: avg_first_response_time_summary,
|
||||||
|
avg_resolution_time: avg_resolution_time_summary
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def conversation_metrics
|
def conversation_metrics
|
||||||
if params[:type].equal?(:account)
|
if params[:type].equal?(:account)
|
||||||
live_conversations
|
live_conversations
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||||
|
pattr_initialize [:account!, :params!]
|
||||||
|
|
||||||
|
def build
|
||||||
|
set_grouped_conversations_count
|
||||||
|
set_grouped_avg_reply_time
|
||||||
|
set_grouped_avg_first_response_time
|
||||||
|
set_grouped_avg_resolution_time
|
||||||
|
prepare_report
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_grouped_conversations_count
|
||||||
|
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_resolution_time
|
||||||
|
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_first_response_time
|
||||||
|
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_reply_time
|
||||||
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_by_key
|
||||||
|
:user_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def reporting_events
|
||||||
|
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_report
|
||||||
|
account.account_users.each_with_object([]) do |account_user, arr|
|
||||||
|
arr << {
|
||||||
|
id: account_user.user_id,
|
||||||
|
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||||
|
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||||
|
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||||
|
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def average_value_key
|
||||||
|
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class V2::Reports::BaseSummaryBuilder
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def group_by_key
|
||||||
|
# Override this method
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_grouped_average(events)
|
||||||
|
events.group(group_by_key).average(average_value_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def average_value_key
|
||||||
|
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||||
|
pattr_initialize [:account!, :params!]
|
||||||
|
|
||||||
|
def build
|
||||||
|
set_grouped_conversations_count
|
||||||
|
set_grouped_avg_reply_time
|
||||||
|
set_grouped_avg_first_response_time
|
||||||
|
set_grouped_avg_resolution_time
|
||||||
|
prepare_report
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_grouped_conversations_count
|
||||||
|
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_resolution_time
|
||||||
|
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_first_response_time
|
||||||
|
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_grouped_avg_reply_time
|
||||||
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def reporting_events
|
||||||
|
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_by_key
|
||||||
|
'conversations.team_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_report
|
||||||
|
account.teams.each_with_object([]) do |team, arr|
|
||||||
|
arr << {
|
||||||
|
id: team.id,
|
||||||
|
conversations_count: @grouped_conversations_count[team.id],
|
||||||
|
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||||
|
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||||
|
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,11 +10,18 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@agent_bot = Current.account.agent_bots.create!(permitted_params)
|
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@agent_bot.update!(permitted_params)
|
@agent_bot.update!(permitted_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def avatar
|
||||||
|
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
|
||||||
|
@agent_bot
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -30,6 +37,10 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
|
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_avatar_from_url
|
||||||
|
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_agent, except: [:create, :index]
|
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :find_user, only: [:create]
|
|
||||||
before_action :validate_limit, only: [:create]
|
before_action :validate_limit, only: [:create]
|
||||||
before_action :create_user, only: [:create]
|
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
|
||||||
before_action :save_account_user, only: [:create]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@agents = agents
|
@agents = agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def create; end
|
def create
|
||||||
|
builder = AgentBuilder.new(
|
||||||
|
email: new_agent_params['email'],
|
||||||
|
name: new_agent_params['name'],
|
||||||
|
role: new_agent_params['role'],
|
||||||
|
availability: new_agent_params['availability'],
|
||||||
|
auto_offline: new_agent_params['auto_offline'],
|
||||||
|
inviter: current_user,
|
||||||
|
account: Current.account
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.perform
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@agent.update!(agent_params.slice(:name).compact)
|
@agent.update!(agent_params.slice(:name).compact)
|
||||||
@@ -23,6 +33,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bulk_create
|
||||||
|
emails = params[:emails]
|
||||||
|
|
||||||
|
emails.each do |email|
|
||||||
|
builder = AgentBuilder.new(
|
||||||
|
email: email,
|
||||||
|
name: email.split('@').first,
|
||||||
|
inviter: current_user,
|
||||||
|
account: Current.account
|
||||||
|
)
|
||||||
|
begin
|
||||||
|
builder.perform
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
@@ -33,47 +62,34 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
@agent = agents.find(params[:id])
|
@agent = agents.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_user
|
|
||||||
@user = User.find_by(email: new_agent_params[:email])
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: move this to a builder and combine the save account user method into a builder
|
|
||||||
# ensure the account user association is also created in a single transaction
|
|
||||||
def create_user
|
|
||||||
return @user.send_confirmation_instructions if @user
|
|
||||||
|
|
||||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_account_user
|
|
||||||
AccountUser.create!({
|
|
||||||
account_id: Current.account.id,
|
|
||||||
user_id: @user.id,
|
|
||||||
inviter_id: current_user.id
|
|
||||||
}.merge({
|
|
||||||
role: new_agent_params[:role],
|
|
||||||
availability: new_agent_params[:availability],
|
|
||||||
auto_offline: new_agent_params[:auto_offline]
|
|
||||||
}.compact))
|
|
||||||
end
|
|
||||||
|
|
||||||
def agent_params
|
def agent_params
|
||||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_agent_params
|
def new_agent_params
|
||||||
# intial string ensures the password requirements are met
|
|
||||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
|
||||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_limit_for_bulk_create
|
||||||
|
limit_available = params[:emails].count <= available_agent_count
|
||||||
|
|
||||||
|
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
|
||||||
|
end
|
||||||
|
|
||||||
def validate_limit
|
def validate_limit
|
||||||
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent?
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_agent_count
|
||||||
|
Current.account.usage_limits[:agents] - agents.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_add_agent?
|
||||||
|
available_agent_count.positive?
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_user_record(agent)
|
def delete_user_record(agent)
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
||||||
@contact.save!
|
@contact.save!
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
process_avatar
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@contact.assign_attributes(contact_update_params)
|
@contact.assign_attributes(contact_update_params)
|
||||||
@contact.save!
|
@contact.save!
|
||||||
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_avatar
|
def process_avatar_from_url
|
||||||
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
|
|||||||
|
|
||||||
def set_agent
|
def set_agent
|
||||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
||||||
@conversation.update_assignee(@agent)
|
@conversation.assignee = @agent
|
||||||
|
@conversation.save!
|
||||||
render_agent
|
render_agent
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def retry
|
||||||
|
return if message.blank?
|
||||||
|
|
||||||
|
message.update!(status: :sent, content_attributes: {})
|
||||||
|
::SendReplyJob.perform_later(message.id)
|
||||||
|
rescue StandardError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
def translate
|
def translate
|
||||||
return head :ok if already_translated_content_available?
|
return head :ok if already_translated_content_available?
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,26 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status].present?
|
# FIXME: move this logic into a service object
|
||||||
|
if pending_to_open_by_bot?
|
||||||
|
@conversation.bot_handoff!
|
||||||
|
elsif params[:status].present?
|
||||||
set_conversation_status
|
set_conversation_status
|
||||||
@status = @conversation.save!
|
@status = @conversation.save!
|
||||||
else
|
else
|
||||||
@status = @conversation.toggle_status
|
@status = @conversation.toggle_status
|
||||||
end
|
end
|
||||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
assign_conversation if should_assign_conversation?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending_to_open_by_bot?
|
||||||
|
return false unless Current.user.is_a?(AgentBot)
|
||||||
|
|
||||||
|
@conversation.status == 'pending' && params[:status] == 'open'
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_assign_conversation?
|
||||||
|
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_priority
|
def toggle_priority
|
||||||
@@ -110,8 +123,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assign_conversation
|
def assign_conversation
|
||||||
@agent = Current.account.users.find(current_user.id)
|
@conversation.assignee = current_user
|
||||||
@conversation.update_assignee(@agent)
|
@conversation.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
|||||||
:attribute_display_type,
|
:attribute_display_type,
|
||||||
:attribute_key,
|
:attribute_key,
|
||||||
:attribute_model,
|
:attribute_model,
|
||||||
|
:regex_pattern,
|
||||||
|
:regex_cue,
|
||||||
attribute_values: []
|
attribute_values: []
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||||
RESULTS_PER_PAGE = 15
|
RESULTS_PER_PAGE = 15
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :fetch_notification, only: [:update]
|
before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread]
|
||||||
before_action :set_primary_actor, only: [:read_all]
|
before_action :set_primary_actor, only: [:read_all]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
@unread_count = notification_finder.unread_count
|
||||||
@count = notifications.count
|
@notifications = notification_finder.perform
|
||||||
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
@count = notification_finder.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_all
|
def read_all
|
||||||
@@ -28,11 +29,35 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
|||||||
render json: @notification
|
render json: @notification
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread
|
||||||
|
@notification.update(read_at: nil)
|
||||||
|
render json: @notification
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@notification.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_all
|
||||||
|
if params[:type] == 'read'
|
||||||
|
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
|
||||||
|
else
|
||||||
|
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
|
||||||
|
end
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def unread_count
|
def unread_count
|
||||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
@unread_count = notification_finder.unread_count
|
||||||
render json: @unread_count
|
render json: @unread_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snooze
|
||||||
|
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
|
||||||
|
render json: @notification
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_primary_actor
|
def set_primary_actor
|
||||||
@@ -50,7 +75,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
|||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications
|
def notification_finder
|
||||||
@notifications ||= current_user.notifications.where(account_id: current_account.id)
|
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||||
only: [:create], raise: false
|
only: [:create], raise: false
|
||||||
before_action :check_signup_enabled, only: [:create]
|
before_action :check_signup_enabled, only: [:create]
|
||||||
|
before_action :ensure_account_name, only: [:create]
|
||||||
before_action :validate_captcha, only: [:create]
|
before_action :validate_captcha, only: [:create]
|
||||||
before_action :fetch_account, except: [:create]
|
before_action :fetch_account, except: [:create]
|
||||||
before_action :check_authorization, except: [:create]
|
before_action :check_authorization, except: [:create]
|
||||||
|
|
||||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||||
|
CustomExceptions::Account::InvalidParams,
|
||||||
CustomExceptions::Account::UserExists,
|
CustomExceptions::Account::UserExists,
|
||||||
CustomExceptions::Account::UserErrors,
|
CustomExceptions::Account::UserErrors,
|
||||||
with: :render_error_response
|
with: :render_error_response
|
||||||
@@ -38,11 +40,13 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def cache_keys
|
def cache_keys
|
||||||
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
render json: { cache_keys: cache_keys_for_account }, status: :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||||
|
@account.custom_attributes.merge!(custom_attributes_params)
|
||||||
|
@account.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_active_at
|
def update_active_at
|
||||||
@@ -53,7 +57,18 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_cache_keys
|
def ensure_account_name
|
||||||
|
# ensure that account_name and user_full_name is present
|
||||||
|
# this is becuase the account builder and the models validations are not triggered
|
||||||
|
# this change is to align the behaviour with the v2 accounts controller
|
||||||
|
# since these values are not required directly there
|
||||||
|
return if account_params[:account_name].present?
|
||||||
|
return if account_params[:user_full_name].present?
|
||||||
|
|
||||||
|
raise CustomExceptions::Account::InvalidParams.new({})
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_keys_for_account
|
||||||
{
|
{
|
||||||
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
||||||
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
||||||
@@ -70,6 +85,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attributes_params
|
||||||
|
params.permit(:industry, :company_size, :timezone)
|
||||||
|
end
|
||||||
|
|
||||||
def check_signup_enabled
|
def check_signup_enabled
|
||||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
@user.update!(password_params.except(:current_password))
|
@user.update!(password_params.except(:current_password))
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.update!(profile_params)
|
@user.assign_attributes(profile_params)
|
||||||
|
@user.custom_attributes.merge!(custom_attributes_params)
|
||||||
|
@user.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar
|
def avatar
|
||||||
@@ -31,6 +33,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resend_confirmation
|
||||||
|
@user.send_confirmation_instructions unless @user.confirmed?
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_user
|
def set_user
|
||||||
@@ -57,6 +64,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attributes_params
|
||||||
|
params.require(:profile).permit(:phone_number)
|
||||||
|
end
|
||||||
|
|
||||||
def password_params
|
def password_params
|
||||||
params.require(:profile).permit(
|
params.require(:profile).permit(
|
||||||
:current_password,
|
:current_password,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :check_authorization
|
||||||
|
before_action :prepare_builder_params, only: [:agent, :team]
|
||||||
|
|
||||||
|
def agent
|
||||||
|
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||||
|
end
|
||||||
|
|
||||||
|
def team
|
||||||
|
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize :report, :view?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_builder_params
|
||||||
|
@builder_params = {
|
||||||
|
since: permitted_params[:since],
|
||||||
|
until: permitted_params[:until],
|
||||||
|
business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_report_with(builder_class)
|
||||||
|
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||||
|
data = builder.build
|
||||||
|
render json: data
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:since, :until, :business_hours)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
class Api::V2::AccountsController < Api::BaseController
|
||||||
|
include AuthHelper
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||||
|
only: [:create], raise: false
|
||||||
|
before_action :check_signup_enabled, only: [:create]
|
||||||
|
before_action :validate_captcha, only: [:create]
|
||||||
|
before_action :fetch_account, except: [:create]
|
||||||
|
before_action :check_authorization, except: [:create]
|
||||||
|
|
||||||
|
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||||
|
CustomExceptions::Account::UserExists,
|
||||||
|
CustomExceptions::Account::UserErrors,
|
||||||
|
with: :render_error_response
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user, @account = AccountBuilder.new(
|
||||||
|
email: account_params[:email],
|
||||||
|
user_password: account_params[:password],
|
||||||
|
locale: account_params[:locale],
|
||||||
|
user: current_user
|
||||||
|
).perform
|
||||||
|
|
||||||
|
fetch_account_and_user_info
|
||||||
|
|
||||||
|
if @user
|
||||||
|
send_auth_headers(@user)
|
||||||
|
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||||
|
else
|
||||||
|
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_account_and_user_info; end
|
||||||
|
|
||||||
|
def fetch_account
|
||||||
|
@account = current_user.accounts.find(params[:id])
|
||||||
|
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_params
|
||||||
|
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_signup_enabled
|
||||||
|
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_captcha
|
||||||
|
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Api::V2::AccountsController.prepend_mod_with('Api::V2::AccountsController')
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
module AccessTokenAuthHelper
|
module AccessTokenAuthHelper
|
||||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||||
'api/v1/accounts/conversations' => %w[toggle_status create],
|
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create],
|
||||||
'api/v1/accounts/conversations/messages' => ['create']
|
'api/v1/accounts/conversations/messages' => ['create'],
|
||||||
|
'api/v1/accounts/conversations/assignments' => ['create']
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def ensure_access_token
|
def ensure_access_token
|
||||||
@@ -14,7 +15,14 @@ module AccessTokenAuthHelper
|
|||||||
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
|
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
|
||||||
|
|
||||||
@resource = @access_token.owner
|
@resource = @access_token.owner
|
||||||
Current.user = @resource if current_user.is_a?(User)
|
Current.user = @resource if allowed_current_user_type?(@resource)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_current_user_type?(resource)
|
||||||
|
return true if resource.is_a?(User)
|
||||||
|
return true if resource.is_a?(AgentBot)
|
||||||
|
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_bot_access_token!
|
def validate_bot_access_token!
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_accessible_for_bot?(account)
|
def account_accessible_for_bot?(account)
|
||||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = AgentBot.new(agent_bot_params)
|
@resource = AgentBot.new(agent_bot_params.except(:avatar_url))
|
||||||
@resource.save!
|
@resource.save!
|
||||||
|
process_avatar_from_url
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(agent_bot_params)
|
@resource.update!(agent_bot_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -23,6 +25,11 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def avatar
|
||||||
|
@resource.avatar.purge if @resource.avatar.attached?
|
||||||
|
@resource
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_resource
|
def set_resource
|
||||||
@@ -30,6 +37,10 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def agent_bot_params
|
def agent_bot_params
|
||||||
params.permit(:name, :description, :account_id, :outgoing_url)
|
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_avatar_from_url
|
||||||
|
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Public::Api::V1::Portals::BaseController < PublicController
|
class Public::Api::V1::Portals::BaseController < PublicController
|
||||||
before_action :show_plain_layout
|
before_action :show_plain_layout
|
||||||
before_action :set_color_scheme
|
before_action :set_color_scheme
|
||||||
|
before_action :set_global_config
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
after_action :allow_iframe_requests
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
@@ -11,11 +12,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_color_scheme
|
def set_color_scheme
|
||||||
@theme = if %w[dark light].include?(params[:theme])
|
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme])
|
||||||
params[:theme]
|
|
||||||
else
|
|
||||||
'system'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
@@ -64,4 +61,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
portal
|
portal
|
||||||
render 'public/api/v1/portals/error/404', status: :not_found
|
render 'public/api/v1/portals/error/404', status: :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_global_config
|
||||||
|
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||||
|
before_action :set_config
|
||||||
|
before_action :allowed_configs
|
||||||
def show
|
def show
|
||||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
|
||||||
# ref: https://github.com/rubocop/rubocop/issues/7767
|
# ref: https://github.com/rubocop/rubocop/issues/7767
|
||||||
# rubocop:disable Style/HashTransformValues
|
# rubocop:disable Style/HashTransformValues
|
||||||
@fb_config = InstallationConfig.where(name: @allowed_configs)
|
@app_config = InstallationConfig.where(name: @allowed_configs)
|
||||||
.pluck(:name, :serialized_value)
|
.pluck(:name, :serialized_value)
|
||||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||||
.to_h
|
.to_h
|
||||||
# rubocop:enable Style/HashTransformValues
|
# rubocop:enable Style/HashTransformValues
|
||||||
|
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
|
||||||
|
result[config_hash['name']] = config_hash.except('name')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
params['app_config'].each do |key, value|
|
params['app_config'].each do |key, value|
|
||||||
|
next unless @allowed_configs.include?(key)
|
||||||
|
|
||||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||||
i.value = value
|
i.value = value
|
||||||
i.save!
|
i.save!
|
||||||
end
|
end
|
||||||
redirect_to super_admin_app_config_url
|
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_config
|
||||||
|
@config = params[:config] || 'general'
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_configs
|
||||||
|
@allowed_configs = case @config
|
||||||
|
when 'facebook'
|
||||||
|
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||||
|
when 'email'
|
||||||
|
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||||
|
else
|
||||||
|
%w[ENABLE_ACCOUNT_SIGNUP]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
|
||||||
|
|||||||
@@ -20,4 +20,13 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
|||||||
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invalid_action_perfomed
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
flash[:error] = 'Invalid action performed'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
true
|
true
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@error_message = e.message
|
Rails.logger.error e.message
|
||||||
|
@error_message = 'Invalid credentials. Please try again.'
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
|
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
|
||||||
|
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
|
||||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||||
# For example, you may want to send an email after a foo is updated.
|
# For example, you may want to send an email after a foo is updated.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sha
|
def sha
|
||||||
sha = `git rev-parse HEAD`
|
@metrics['Git SHA'] = GIT_HASH
|
||||||
@metrics['Git SHA'] = sha.presence || 'n/a'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def postgres_status
|
def postgres_status
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def refresh
|
||||||
|
Internal::CheckNewVersionsJob.perform_now
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -30,11 +30,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
|
|||||||
# SHOW_PAGE_ATTRIBUTES
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed on the model's show page.
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
SHOW_PAGE_ATTRIBUTES = %i[
|
SHOW_PAGE_ATTRIBUTES = %i[
|
||||||
owner
|
|
||||||
id
|
|
||||||
token
|
token
|
||||||
created_at
|
|
||||||
updated_at
|
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
outgoing_url
|
outgoing_url
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PlatformAppDashboard < Administrate::BaseDashboard
|
|||||||
name
|
name
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
updated_at: Field::DateTime,
|
updated_at: Field::DateTime,
|
||||||
pubsub_token: Field::String,
|
pubsub_token: Field::String,
|
||||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||||
accounts: CountField
|
accounts: CountField,
|
||||||
|
access_token: Field::HasOne
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
# COLLECTION_ATTRIBUTES
|
# COLLECTION_ATTRIBUTES
|
||||||
@@ -67,6 +68,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
updated_at
|
updated_at
|
||||||
confirmed_at
|
confirmed_at
|
||||||
account_users
|
account_users
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -18,4 +18,9 @@ class ContactDrop < BaseDrop
|
|||||||
def last_name
|
def last_name
|
||||||
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attribute
|
||||||
|
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||||
|
custom_attributes.transform_keys(&:to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class ConversationDrop < BaseDrop
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attribute
|
||||||
|
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||||
|
custom_attributes.transform_keys(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def message_sender_name(sender)
|
def message_sender_name(sender)
|
||||||
|
|||||||
@@ -3,13 +3,21 @@ class ConversationFinder
|
|||||||
|
|
||||||
DEFAULT_STATUS = 'open'.freeze
|
DEFAULT_STATUS = 'open'.freeze
|
||||||
SORT_OPTIONS = {
|
SORT_OPTIONS = {
|
||||||
latest: 'latest',
|
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
|
||||||
sort_on_created_at: 'sort_on_created_at',
|
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
|
||||||
last_user_message_at: 'last_user_message_at',
|
'created_at_asc' => %w[sort_on_created_at asc],
|
||||||
sort_on_priority: 'sort_on_priority',
|
'created_at_desc' => %w[sort_on_created_at desc],
|
||||||
sort_on_waiting_since: 'sort_on_waiting_since'
|
'priority_asc' => %w[sort_on_priority asc],
|
||||||
}.with_indifferent_access
|
'priority_desc' => %w[sort_on_priority desc],
|
||||||
|
'waiting_since_asc' => %w[sort_on_waiting_since asc],
|
||||||
|
'waiting_since_desc' => %w[sort_on_waiting_since desc],
|
||||||
|
|
||||||
|
# To be removed in v3.5.0
|
||||||
|
'latest' => %w[sort_on_last_activity_at desc],
|
||||||
|
'sort_on_created_at' => %w[sort_on_created_at asc],
|
||||||
|
'sort_on_priority' => %w[sort_on_priority desc],
|
||||||
|
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
|
||||||
|
}.with_indifferent_access
|
||||||
# assumptions
|
# assumptions
|
||||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||||
# assignee_type if not given, take 'all'
|
# assignee_type if not given, take 'all'
|
||||||
@@ -159,7 +167,8 @@ class ConversationFinder
|
|||||||
@conversations = @conversations.includes(
|
@conversations = @conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||||
)
|
)
|
||||||
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
|
|
||||||
@conversations.send(sort_by).page(current_page)
|
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||||
|
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class NotificationFinder
|
||||||
|
attr_reader :current_user, :current_account, :params
|
||||||
|
|
||||||
|
RESULTS_PER_PAGE = 15
|
||||||
|
|
||||||
|
def initialize(current_user, current_account, params = {})
|
||||||
|
@current_user = current_user
|
||||||
|
@current_account = current_account
|
||||||
|
@params = params
|
||||||
|
set_up
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
notifications
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_count
|
||||||
|
@notifications.where(read_at: nil).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def count
|
||||||
|
@notifications.count
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_up
|
||||||
|
find_all_notifications
|
||||||
|
filter_by_read_status
|
||||||
|
filter_by_status
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_all_notifications
|
||||||
|
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_status
|
||||||
|
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_read_status
|
||||||
|
@notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read'
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_page
|
||||||
|
params[:page] || 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def notifications
|
||||||
|
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -53,7 +53,7 @@ module Api::V1::InboxesHelper
|
|||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
raise StandardError, e.message
|
raise StandardError, e.message
|
||||||
ensure
|
ensure
|
||||||
ChatwootExceptionTracker.new(e).capture_exception if e.present?
|
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_smtp_connection(channel_data, smtp)
|
def check_smtp_connection(channel_data, smtp)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
dates = data.pluck(:date).uniq.sort
|
dates = data.pluck(:date).uniq.sort
|
||||||
|
|
||||||
# add the dates as the first row, leave an empty cell for the hour column
|
# add the dates as the first row, leave an empty cell for the hour column
|
||||||
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
|
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03']
|
||||||
result_arr << ([nil] + dates)
|
result_arr << (['Start of the hour'] + dates)
|
||||||
|
|
||||||
# group the data by hour, we do not need to sort it, because the data is already sorted
|
# group the data by hour, we do not need to sort it, because the data is already sorted
|
||||||
# given it starts from the beginning of the day
|
# given it starts from the beginning of the day
|
||||||
@@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
||||||
data.group_by { |d| d[:hour] }.each do |hour, items|
|
data.group_by { |d| d[:hour] }.each do |hour, items|
|
||||||
# create a new row for each hour
|
# create a new row for each hour
|
||||||
row = [hour]
|
row = [format('%02d:00', hour)]
|
||||||
|
|
||||||
# group the items by date, so we can easily access the value for each date
|
# group the items by date, so we can easily access the value for each date
|
||||||
# grouped values will be a hasg with the date as the key, and the value as the value
|
# grouped values will be a hasg with the date as the key, and the value as the value
|
||||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
||||||
end
|
end
|
||||||
|
|
||||||
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
|
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4]
|
||||||
# add the row to the result array
|
# add the row to the result array
|
||||||
|
|
||||||
result_arr << row
|
result_arr << row
|
||||||
@@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
# return the resultant array
|
# return the resultant array
|
||||||
# the result looks like this
|
# the result looks like this
|
||||||
# [
|
# [
|
||||||
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
|
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||||
# [0, 0, 0, 0],
|
# ['00:00', 0, 0, 0],
|
||||||
# [1, 0, 0, 0],
|
# ['01:00', 0, 0, 0],
|
||||||
# [2, 0, 0, 0],
|
# ['02:00', 0, 0, 0],
|
||||||
# [3, 0, 0, 0],
|
# ['03:00', 0, 0, 0],
|
||||||
# [4, 0, 0, 0],
|
# ['04:00', 0, 0, 0],
|
||||||
# ]
|
# ]
|
||||||
result_arr
|
result_arr
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::ReportsHelper
|
|||||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).summary
|
).short_summary
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -45,12 +45,8 @@ module Api::V2::Accounts::ReportsHelper
|
|||||||
def generate_readable_report_metrics(report_metric)
|
def generate_readable_report_metrics(report_metric)
|
||||||
[
|
[
|
||||||
report_metric[:conversations_count],
|
report_metric[:conversations_count],
|
||||||
time_to_minutes(report_metric[:avg_first_response_time]),
|
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
|
||||||
time_to_minutes(report_metric[:avg_resolution_time])
|
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def time_to_minutes(time_in_seconds)
|
|
||||||
(time_in_seconds / 60).to_i
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ module PortalHelper
|
|||||||
|
|
||||||
def generate_portal_bg(portal_color, theme)
|
def generate_portal_bg(portal_color, theme)
|
||||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_gradient_to_bottom(theme)
|
||||||
|
base_color = theme == 'dark' ? '#151718' : 'white'
|
||||||
|
"linear-gradient(to bottom, transparent, #{base_color})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_portal_hover_color(portal_color, theme)
|
||||||
|
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
|
||||||
|
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
|
||||||
end
|
end
|
||||||
|
|
||||||
def language_name(locale)
|
def language_name(locale)
|
||||||
@@ -14,35 +24,48 @@ module PortalHelper
|
|||||||
language_map[locale] || locale
|
language_map[locale] || locale
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_theme_names(theme)
|
def theme_query_string(theme)
|
||||||
if theme == 'light'
|
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
|
||||||
I18n.t('public_portal.header.appearance.light')
|
end
|
||||||
elsif theme == 'dark'
|
|
||||||
I18n.t('public_portal.header.appearance.dark')
|
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
|
||||||
|
if is_plain_layout_enabled
|
||||||
|
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
|
||||||
else
|
else
|
||||||
I18n.t('public_portal.header.appearance.system')
|
"/hc/#{portal_slug}/#{portal_locale}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_theme_icon(theme)
|
def generate_category_link(params)
|
||||||
if theme == 'light'
|
portal_slug = params[:portal_slug]
|
||||||
'icons/sun'
|
category_locale = params[:category_locale]
|
||||||
elsif theme == 'dark'
|
category_slug = params[:category_slug]
|
||||||
'icons/moon'
|
theme = params[:theme]
|
||||||
|
is_plain_layout_enabled = params[:is_plain_layout_enabled]
|
||||||
|
|
||||||
|
if is_plain_layout_enabled
|
||||||
|
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
|
||||||
else
|
else
|
||||||
'icons/monitor'
|
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_gradient_to_bottom(theme)
|
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
|
||||||
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
|
if is_plain_layout_enabled
|
||||||
end
|
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
|
||||||
|
else
|
||||||
def generate_article_link(portal_slug, article_slug, theme)
|
"/hc/#{portal_slug}/articles/#{article_slug}"
|
||||||
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_category_content(content)
|
def render_category_content(content)
|
||||||
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def thumbnail_bg_color(username)
|
||||||
|
colors = ['#6D95BA', '#A4C3C3', '#E19191']
|
||||||
|
return colors.sample if username.blank?
|
||||||
|
|
||||||
|
colors[username.length % colors.size]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module SuperAdmin::AccountFeaturesHelper
|
||||||
|
def self.account_features
|
||||||
|
YAML.safe_load(Rails.root.join('config/features.yml').read).freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.account_premium_features
|
||||||
|
account_features.filter { |feature| feature['premium'] }.pluck('name')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,12 +2,13 @@
|
|||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||||
id="app"
|
id="app"
|
||||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
|
||||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<template v-if="currentAccountId">
|
<template v-if="currentAccountId">
|
||||||
|
<pending-email-verification-banner />
|
||||||
<payment-pending-banner />
|
<payment-pending-banner />
|
||||||
<upgrade-banner />
|
<upgrade-banner />
|
||||||
</template>
|
</template>
|
||||||
@@ -32,6 +33,7 @@ import NetworkNotification from './components/NetworkNotification.vue';
|
|||||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||||
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||||
|
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||||
import vueActionCable from './helper/actionCable';
|
import vueActionCable from './helper/actionCable';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
@@ -52,6 +54,7 @@ export default {
|
|||||||
PaymentPendingBanner,
|
PaymentPendingBanner,
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
UpgradeBanner,
|
UpgradeBanner,
|
||||||
|
PendingEmailVerificationBanner,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [rtlMixin],
|
mixins: [rtlMixin],
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ export default {
|
|||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
},
|
},
|
||||||
hasAuthCookie() {
|
hasAuthCookie() {
|
||||||
return !!Cookies.getJSON('cw_d_session_info');
|
return !!Cookies.get('cw_d_session_info');
|
||||||
},
|
},
|
||||||
getAuthData() {
|
getAuthData() {
|
||||||
if (this.hasAuthCookie()) {
|
if (this.hasAuthCookie()) {
|
||||||
return Cookies.getJSON('cw_d_session_info');
|
const savedAuthInfo = Cookies.get('cw_d_session_info');
|
||||||
|
return JSON.parse(savedAuthInfo || '{}');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -97,4 +98,8 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
resendConfirmation() {
|
||||||
|
const urlData = endPoints('resendConfirmation');
|
||||||
|
return axios.post(urlData.url);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ const endPoints = {
|
|||||||
setActiveAccount: {
|
setActiveAccount: {
|
||||||
url: '/api/v1/profile/set_active_account',
|
url: '/api/v1/profile/set_active_account',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resendConfirmation: {
|
||||||
|
url: '/api/v1/profile/resend_confirmation',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page => {
|
export default page => {
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ class MessageApi extends ApiClient {
|
|||||||
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry(conversationID, messageId) {
|
||||||
|
return axios.post(
|
||||||
|
`${this.url}/${conversationID}/messages/${messageId}/retry`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getPreviousMessages({ conversationId, after, before }) {
|
getPreviousMessages({ conversationId, after, before }) {
|
||||||
const params = { before };
|
const params = { before };
|
||||||
if (after && Number(after) !== Number(before)) {
|
if (after && Number(after) !== Number(before)) {
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ class NotificationsAPI extends ApiClient {
|
|||||||
super('notifications', { accountScoped: true });
|
super('notifications', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get(page) {
|
get({ page, status, type, sortOrder }) {
|
||||||
return axios.get(`${this.url}?page=${page}`);
|
return axios.get(this.url, {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
status,
|
||||||
|
type,
|
||||||
|
sort_order: sortOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotifications(contactId) {
|
getNotifications(contactId) {
|
||||||
@@ -25,9 +32,29 @@ class NotificationsAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unRead(id) {
|
||||||
|
return axios.post(`${this.url}/${id}/unread`);
|
||||||
|
}
|
||||||
|
|
||||||
readAll() {
|
readAll() {
|
||||||
return axios.post(`${this.url}/read_all`);
|
return axios.post(`${this.url}/read_all`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
return axios.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll({ type = 'all' }) {
|
||||||
|
return axios.post(`${this.url}/destroy_all`, {
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
snooze({ id, snoozedUntil = null }) {
|
||||||
|
return axios.post(`${this.url}/${id}/snooze`, {
|
||||||
|
snoozed_until: snoozedUntil,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NotificationsAPI();
|
export default new NotificationsAPI();
|
||||||
|
|||||||
@@ -28,10 +28,20 @@ describe('#NotificationAPI', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('#get', () => {
|
it('#get', () => {
|
||||||
notificationsAPI.get(1);
|
notificationsAPI.get({
|
||||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
page: 1,
|
||||||
'/api/v1/notifications?page=1'
|
status: 'read',
|
||||||
);
|
type: 'Conversation',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||||
|
params: {
|
||||||
|
page: 1,
|
||||||
|
status: 'read',
|
||||||
|
type: 'Conversation',
|
||||||
|
sort_order: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getNotifications', () => {
|
it('#getNotifications', () => {
|
||||||
@@ -65,5 +75,30 @@ describe('#NotificationAPI', () => {
|
|||||||
'/api/v1/notifications/read_all'
|
'/api/v1/notifications/read_all'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#snooze', () => {
|
||||||
|
notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 });
|
||||||
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/notifications/1/snooze',
|
||||||
|
{
|
||||||
|
snoozed_until: 12332211,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#delete', () => {
|
||||||
|
notificationsAPI.delete(1);
|
||||||
|
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#deleteAll', () => {
|
||||||
|
notificationsAPI.deleteAll({ type: 'all' });
|
||||||
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/notifications/destroy_all',
|
||||||
|
{
|
||||||
|
type: 'all',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,39 +1,8 @@
|
|||||||
@import '../variables';
|
@import 'shared/assets/fonts/inter';
|
||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
.superadmin-body {
|
body {
|
||||||
background: var(--color-background);
|
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
|
||||||
.hero--title {
|
|
||||||
font-size: var(--font-size-mega);
|
|
||||||
font-weight: var(--font-weight-light);
|
|
||||||
margin-top: var(--space-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-subscription--checkbox {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-right: var(--space-one);
|
|
||||||
margin-top: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-box {
|
|
||||||
background-color: var(--r-500);
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--color-white);
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
@import 'shared/assets/fonts/plus-jakarta';
|
|
||||||
@import '../variables';
|
|
||||||
@import '~shared/assets/stylesheets/ionicons';
|
|
||||||
@@ -76,7 +76,11 @@
|
|||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
.bubble {
|
.bubble {
|
||||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words;
|
@apply rounded-r-lg rounded-l mr-auto break-words;
|
||||||
|
|
||||||
|
&:not(.is-unsupported) {
|
||||||
|
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
|
||||||
|
}
|
||||||
|
|
||||||
&.is-image {
|
&.is-image {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.modal-mask {
|
.modal-mask {
|
||||||
// @include flex;
|
// @include flex;
|
||||||
// @include flex-align(center, middle);
|
// @include flex-align(center, middle);
|
||||||
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
|
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-top-bar {
|
.page-top-bar {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||||
:class="{
|
:class="{
|
||||||
hide: !showConversationList,
|
hide: !showConversationList,
|
||||||
'list--full-width': isOnExpandedLayout,
|
'list--full-width': isOnExpandedLayout,
|
||||||
@@ -126,49 +126,33 @@
|
|||||||
@assign-team="onAssignTeamsForBulk"
|
@assign-team="onAssignTeamsForBulk"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="activeConversation"
|
ref="conversationList"
|
||||||
class="conversations-list flex-1"
|
class="conversations-list flex-1"
|
||||||
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
||||||
>
|
>
|
||||||
<div>
|
<virtual-list
|
||||||
<conversation-card
|
ref="conversationVirtualList"
|
||||||
v-for="chat in conversationList"
|
:data-key="'id'"
|
||||||
:key="chat.id"
|
:data-sources="conversationList"
|
||||||
:active-label="label"
|
:data-component="itemComponent"
|
||||||
:team-id="teamId"
|
:extra-props="virtualListExtraProps"
|
||||||
:folders-id="foldersId"
|
class="w-full overflow-auto h-full"
|
||||||
:chat="chat"
|
footer-tag="div"
|
||||||
:conversation-type="conversationType"
|
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
|
||||||
:selected="isConversationSelected(chat.id)"
|
|
||||||
@select-conversation="selectConversation"
|
|
||||||
@de-select-conversation="deSelectConversation"
|
|
||||||
@assign-agent="onAssignAgent"
|
|
||||||
@assign-team="onAssignTeam"
|
|
||||||
@assign-label="onAssignLabels"
|
|
||||||
@update-conversation-status="toggleConversationStatus"
|
|
||||||
@context-menu-toggle="onContextMenuToggle"
|
|
||||||
@mark-as-unread="markAsUnread"
|
|
||||||
@assign-priority="assignPriority"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
|
||||||
<span class="spinner mt-4 mb-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<woot-button
|
|
||||||
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
|
||||||
variant="clear"
|
|
||||||
size="expanded"
|
|
||||||
class="load-more--button"
|
|
||||||
@click="loadMoreConversations"
|
|
||||||
>
|
>
|
||||||
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
<template #footer>
|
||||||
</woot-button>
|
<div v-if="chatListLoading" class="text-center">
|
||||||
|
<span class="spinner mt-4 mb-4" />
|
||||||
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
|
</div>
|
||||||
{{ $t('CHAT_LIST.EOF') }}
|
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
|
||||||
</p>
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
|
</p>
|
||||||
|
<intersection-observer
|
||||||
|
v-if="!showEndOfListMessage && !chatListLoading"
|
||||||
|
:options="infiniteLoaderOptions"
|
||||||
|
@observed="loadMoreConversations"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</virtual-list>
|
||||||
</div>
|
</div>
|
||||||
<woot-modal
|
<woot-modal
|
||||||
:show.sync="showAdvancedFilters"
|
:show.sync="showAdvancedFilters"
|
||||||
@@ -191,11 +175,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import VirtualList from 'vue-virtual-scroll-list';
|
||||||
|
|
||||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
import ConversationCard from './widgets/conversation/ConversationCard.vue';
|
import ConversationItem from './ConversationItem.vue';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
@@ -222,16 +207,20 @@ import {
|
|||||||
isOnUnattendedView,
|
isOnUnattendedView,
|
||||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||||
|
import IntersectionObserver from './IntersectionObserver.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AddCustomViews,
|
AddCustomViews,
|
||||||
ChatTypeTabs,
|
ChatTypeTabs,
|
||||||
ConversationCard,
|
// eslint-disable-next-line vue/no-unused-components
|
||||||
|
ConversationItem,
|
||||||
ConversationAdvancedFilter,
|
ConversationAdvancedFilter,
|
||||||
DeleteCustomViews,
|
DeleteCustomViews,
|
||||||
ConversationBulkActions,
|
ConversationBulkActions,
|
||||||
ConversationBasicFilter,
|
ConversationBasicFilter,
|
||||||
|
IntersectionObserver,
|
||||||
|
VirtualList,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
timeMixin,
|
timeMixin,
|
||||||
@@ -241,6 +230,20 @@ export default {
|
|||||||
filterMixin,
|
filterMixin,
|
||||||
uiSettingsMixin,
|
uiSettingsMixin,
|
||||||
],
|
],
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
// Actions to be performed on virtual list item and context menu.
|
||||||
|
selectConversation: this.selectConversation,
|
||||||
|
deSelectConversation: this.deSelectConversation,
|
||||||
|
assignAgent: this.onAssignAgent,
|
||||||
|
assignTeam: this.onAssignTeam,
|
||||||
|
assignLabels: this.onAssignLabels,
|
||||||
|
updateConversationStatus: this.toggleConversationStatus,
|
||||||
|
toggleContextMenu: this.onContextMenuToggle,
|
||||||
|
markAsUnread: this.markAsUnread,
|
||||||
|
assignPriority: this.assignPriority,
|
||||||
|
};
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -275,7 +278,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||||
activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
|
activeSortBy: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC,
|
||||||
showAdvancedFilters: false,
|
showAdvancedFilters: false,
|
||||||
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
||||||
...filter,
|
...filter,
|
||||||
@@ -291,6 +294,21 @@ export default {
|
|||||||
selectedInboxes: [],
|
selectedInboxes: [],
|
||||||
isContextMenuOpen: false,
|
isContextMenuOpen: false,
|
||||||
appliedFilter: [],
|
appliedFilter: [],
|
||||||
|
infiniteLoaderOptions: {
|
||||||
|
root: this.$refs.conversationList,
|
||||||
|
rootMargin: '100px 0px 100px 0px',
|
||||||
|
},
|
||||||
|
|
||||||
|
itemComponent: ConversationItem,
|
||||||
|
// virtualListExtraProps is to pass the props to the conversationItem component.
|
||||||
|
virtualListExtraProps: {
|
||||||
|
label: this.label,
|
||||||
|
teamId: this.teamId,
|
||||||
|
foldersId: this.foldersId,
|
||||||
|
conversationType: this.conversationType,
|
||||||
|
showAssignee: false,
|
||||||
|
isConversationSelected: this.isConversationSelected,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -501,6 +519,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
teamId() {
|
||||||
|
this.updateVirtualListProps('teamId', this.teamId);
|
||||||
|
},
|
||||||
activeTeam() {
|
activeTeam() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
},
|
},
|
||||||
@@ -509,16 +530,22 @@ export default {
|
|||||||
},
|
},
|
||||||
label() {
|
label() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('label', this.label);
|
||||||
},
|
},
|
||||||
conversationType() {
|
conversationType() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('conversationType', this.conversationType);
|
||||||
},
|
},
|
||||||
activeFolder() {
|
activeFolder() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('foldersId', this.foldersId);
|
||||||
},
|
},
|
||||||
chatLists() {
|
chatLists() {
|
||||||
this.chatsOnView = this.conversationList;
|
this.chatsOnView = this.conversationList;
|
||||||
},
|
},
|
||||||
|
showAssigneeInConversationCard(newVal) {
|
||||||
|
this.updateVirtualListProps('showAssignee', newVal);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setFiltersFromUISettings();
|
this.setFiltersFromUISettings();
|
||||||
@@ -535,6 +562,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateVirtualListProps(key, value) {
|
||||||
|
this.virtualListExtraProps = {
|
||||||
|
...this.virtualListExtraProps,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
},
|
||||||
onApplyFilter(payload) {
|
onApplyFilter(payload) {
|
||||||
this.resetBulkActions();
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
@@ -555,7 +588,10 @@ export default {
|
|||||||
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
|
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
|
||||||
const { status, order_by: orderBy } = filterBy;
|
const { status, order_by: orderBy } = filterBy;
|
||||||
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
|
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
|
||||||
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
|
this.activeSortBy =
|
||||||
|
Object.keys(wootConstants.SORT_BY_TYPE).find(
|
||||||
|
sortField => sortField === orderBy
|
||||||
|
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
||||||
},
|
},
|
||||||
onClickOpenAddFoldersModal() {
|
onClickOpenAddFoldersModal() {
|
||||||
this.showAddFoldersModal = true;
|
this.showAddFoldersModal = true;
|
||||||
@@ -635,10 +671,10 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
getKeyboardListenerParams() {
|
getKeyboardListenerParams() {
|
||||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
const allConversations = this.$refs.conversationList.querySelectorAll(
|
||||||
'div.conversations-list div.conversation'
|
'div.conversations-list div.conversation'
|
||||||
);
|
);
|
||||||
const activeConversation = this.$refs.activeConversation.querySelector(
|
const activeConversation = this.$refs.conversationList.querySelector(
|
||||||
'div.conversations-list div.conversation.active'
|
'div.conversations-list div.conversation.active'
|
||||||
);
|
);
|
||||||
const activeConversationIndex = [...allConversations].indexOf(
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
@@ -694,9 +730,12 @@ export default {
|
|||||||
fetchConversations() {
|
fetchConversations() {
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('fetchAllConversations', this.conversationFilters)
|
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
},
|
},
|
||||||
loadMoreConversations() {
|
loadMoreConversations() {
|
||||||
|
if (this.hasCurrentPageEndReached || this.chatListLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
}
|
}
|
||||||
@@ -715,7 +754,7 @@ export default {
|
|||||||
queryData: filterQueryGenerator(payload),
|
queryData: filterQueryGenerator(payload),
|
||||||
page,
|
page,
|
||||||
})
|
})
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
this.showAdvancedFilters = false;
|
this.showAdvancedFilters = false;
|
||||||
},
|
},
|
||||||
fetchSavedFilteredConversations(payload) {
|
fetchSavedFilteredConversations(payload) {
|
||||||
@@ -725,7 +764,7 @@ export default {
|
|||||||
queryData: payload,
|
queryData: payload,
|
||||||
page,
|
page,
|
||||||
})
|
})
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
},
|
},
|
||||||
updateAssigneeTab(selectedTab) {
|
updateAssigneeTab(selectedTab) {
|
||||||
if (this.activeAssigneeTab !== selectedTab) {
|
if (this.activeAssigneeTab !== selectedTab) {
|
||||||
@@ -737,6 +776,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emitConversationLoaded() {
|
||||||
|
this.$emit('conversation-load');
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Addressing a known issue in the virtual list library where dynamically added items
|
||||||
|
// might not render correctly. This workaround involves a slight manual adjustment
|
||||||
|
// to the scroll position, triggering the list to refresh its rendering.
|
||||||
|
const virtualList = this.$refs.conversationVirtualList;
|
||||||
|
const scrollToOffset = virtualList?.scrollToOffset;
|
||||||
|
const currentOffset = virtualList?.getOffset() || 0;
|
||||||
|
if (scrollToOffset) {
|
||||||
|
scrollToOffset(currentOffset + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
resetBulkActions() {
|
resetBulkActions() {
|
||||||
this.selectedConversations = [];
|
this.selectedConversations = [];
|
||||||
this.selectedInboxes = [];
|
this.selectedInboxes = [];
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<conversation-card
|
||||||
|
:key="source.id"
|
||||||
|
:active-label="label"
|
||||||
|
:team-id="teamId"
|
||||||
|
:folders-id="foldersId"
|
||||||
|
:chat="source"
|
||||||
|
:conversation-type="conversationType"
|
||||||
|
:selected="isConversationSelected(source.id)"
|
||||||
|
:show-assignee="showAssignee"
|
||||||
|
:enable-context-menu="true"
|
||||||
|
@select-conversation="selectConversation"
|
||||||
|
@de-select-conversation="deSelectConversation"
|
||||||
|
@assign-agent="assignAgent"
|
||||||
|
@assign-team="assignTeam"
|
||||||
|
@assign-label="assignLabels"
|
||||||
|
@update-conversation-status="updateConversationStatus"
|
||||||
|
@context-menu-toggle="toggleContextMenu"
|
||||||
|
@mark-as-unread="markAsUnread"
|
||||||
|
@assign-priority="assignPriority"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConversationCard from './widgets/conversation/ConversationCard.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ConversationCard,
|
||||||
|
},
|
||||||
|
inject: [
|
||||||
|
'selectConversation',
|
||||||
|
'deSelectConversation',
|
||||||
|
'assignAgent',
|
||||||
|
'assignTeam',
|
||||||
|
'assignLabels',
|
||||||
|
'updateConversationStatus',
|
||||||
|
'toggleContextMenu',
|
||||||
|
'markAsUnread',
|
||||||
|
'assignPriority',
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
source: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
teamId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
conversationType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
foldersId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isConversationSelected: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
showAssignee: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -126,18 +126,26 @@ import { required, url } from 'vuelidate/lib/validators';
|
|||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
import { isValidURL } from '../helper/URLHelper';
|
import { isValidURL } from '../helper/URLHelper';
|
||||||
|
import customAttributeMixin from '../mixins/customAttributeMixin';
|
||||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MultiselectDropdown,
|
MultiselectDropdown,
|
||||||
},
|
},
|
||||||
|
mixins: [customAttributeMixin],
|
||||||
props: {
|
props: {
|
||||||
label: { type: String, required: true },
|
label: { type: String, required: true },
|
||||||
values: { type: Array, default: () => [] },
|
values: { type: Array, default: () => [] },
|
||||||
value: { type: [String, Number, Boolean], default: '' },
|
value: { type: [String, Number, Boolean], default: '' },
|
||||||
showActions: { type: Boolean, default: false },
|
showActions: { type: Boolean, default: false },
|
||||||
attributeType: { type: String, default: 'text' },
|
attributeType: { type: String, default: 'text' },
|
||||||
|
attributeRegex: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
regexCue: { type: String, default: null },
|
||||||
|
regexEnabled: { type: Boolean, default: false },
|
||||||
attributeKey: { type: String, required: true },
|
attributeKey: { type: String, required: true },
|
||||||
contactId: { type: Number, default: null },
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
@@ -204,6 +212,11 @@ export default {
|
|||||||
if (this.$v.editedValue.url) {
|
if (this.$v.editedValue.url) {
|
||||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
||||||
}
|
}
|
||||||
|
if (!this.$v.editedValue.regexValidation) {
|
||||||
|
return this.regexCue
|
||||||
|
? this.regexCue
|
||||||
|
: this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
|
||||||
|
}
|
||||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -221,7 +234,15 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
editedValue: { required },
|
editedValue: {
|
||||||
|
required,
|
||||||
|
regexValidation: value => {
|
||||||
|
return !(
|
||||||
|
this.attributeRegex &&
|
||||||
|
!this.getRegexp(this.attributeRegex).test(value)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="observedElement" class="h-6 w-full" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
this.registerInfiniteLoader();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unobserveInfiniteLoadObserver();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
registerInfiniteLoader() {
|
||||||
|
this.intersectionObserver = new IntersectionObserver(entries => {
|
||||||
|
if (entries && entries[0].isIntersecting) {
|
||||||
|
this.$emit('observed');
|
||||||
|
}
|
||||||
|
}, this.options);
|
||||||
|
this.intersectionObserver.observe(this.$refs.observedElement);
|
||||||
|
},
|
||||||
|
unobserveInfiniteLoadObserver() {
|
||||||
|
this.intersectionObserver.unobserve(this.$refs.observedElement);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,24 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="network-notification-fade" tag="div">
|
<transition name="network-notification-fade" tag="div">
|
||||||
<div v-show="showNotification" class="ui-notification-container">
|
<div v-show="showNotification" class="fixed top-4 left-2 z-50 group">
|
||||||
<div class="ui-notification">
|
<div
|
||||||
<fluent-icon icon="wifi-off" />
|
class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative"
|
||||||
<p class="ui-notification-text">
|
>
|
||||||
{{
|
<fluent-icon
|
||||||
useInstallationName(
|
icon="wifi-off"
|
||||||
$t('NETWORK.NOTIFICATION.TEXT'),
|
class="text-yellow-700/50 dark:text-yellow-50"
|
||||||
globalConfig.installationName
|
size="18"
|
||||||
)
|
/>
|
||||||
}}
|
<span
|
||||||
</p>
|
class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50"
|
||||||
<woot-button variant="clear" size="small" @click="refreshPage">
|
>
|
||||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
{{ $t('NETWORK.NOTIFICATION.OFFLINE') }}
|
||||||
</woot-button>
|
</span>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="smooth"
|
:title="$t('NETWORK.BUTTON.REFRESH')"
|
||||||
|
variant="clear"
|
||||||
size="small"
|
size="small"
|
||||||
color-scheme="warning"
|
color-scheme="warning"
|
||||||
icon="dismiss-circle"
|
icon="arrow-clockwise"
|
||||||
|
class="visible transition-all duration-500 ease-in-out ml-1"
|
||||||
|
@click="refreshPage"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
size="small"
|
||||||
|
color-scheme="warning"
|
||||||
|
icon="dismiss"
|
||||||
@click="closeNotification"
|
@click="closeNotification"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +56,12 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('offline', this.updateOnlineStatus);
|
window.addEventListener('offline', this.updateOnlineStatus);
|
||||||
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
|
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
|
||||||
this.updateOnlineStatus({ type: 'offline' });
|
// TODO: Remove this after completing the conversation list refetching
|
||||||
|
// TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW
|
||||||
|
// THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateOnlineStatus({ type: 'offline' });
|
||||||
|
}, 4000);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -72,33 +86,3 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import '~dashboard/assets/scss/mixins';
|
|
||||||
|
|
||||||
.ui-notification-container {
|
|
||||||
max-width: 25rem;
|
|
||||||
position: absolute;
|
|
||||||
right: var(--space-normal);
|
|
||||||
top: var(--space-normal);
|
|
||||||
z-index: var(--z-index-very-high);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-notification {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
background-color: var(--y-100);
|
|
||||||
border-radius: var(--border-radius-medium);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
|
|
||||||
min-width: 15rem;
|
|
||||||
padding: var(--space-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-notification-text {
|
|
||||||
margin: 0 var(--space-small);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<banner
|
||||||
|
v-if="shouldShowBanner"
|
||||||
|
color-scheme="alert"
|
||||||
|
:banner-message="bannerMessage"
|
||||||
|
:action-button-label="actionButtonMessage"
|
||||||
|
action-button-icon="mail"
|
||||||
|
has-action-button
|
||||||
|
@click="resendVerificationEmail"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import accountMixin from 'dashboard/mixins/account';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Banner },
|
||||||
|
mixins: [accountMixin, alertMixin],
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
|
}),
|
||||||
|
bannerMessage() {
|
||||||
|
return this.$t('APP_GLOBAL.EMAIL_VERIFICATION_PENDING');
|
||||||
|
},
|
||||||
|
actionButtonMessage() {
|
||||||
|
return this.$t('APP_GLOBAL.RESEND_VERIFICATION_MAIL');
|
||||||
|
},
|
||||||
|
shouldShowBanner() {
|
||||||
|
return !this.currentUser.confirmed;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resendVerificationEmail() {
|
||||||
|
this.$store.dispatch('resendConfirmation');
|
||||||
|
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+1
-1
@@ -56,7 +56,7 @@
|
|||||||
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
|
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
|
||||||
:class="
|
:class="
|
||||||
isCountZero
|
isCountZero
|
||||||
? `text-slate-300 dark:text-slate-700`
|
? `text-slate-300 dark:text-slate-500`
|
||||||
: `text-slate-700 dark:text-slate-50`
|
: `text-slate-700 dark:text-slate-50`
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="banner flex items-center h-12 gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center"
|
class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white"
|
||||||
:class="bannerClasses"
|
:class="bannerClasses"
|
||||||
>
|
>
|
||||||
<span class="banner-message">
|
<span class="banner-message">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<woot-button
|
<woot-button
|
||||||
v-if="hasActionButton"
|
v-if="hasActionButton"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
icon="arrow-right"
|
:icon="actionButtonIcon"
|
||||||
:variant="actionButtonVariant"
|
:variant="actionButtonVariant"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
class-names="banner-action__button"
|
class-names="banner-action__button"
|
||||||
@@ -67,6 +67,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
actionButtonIcon: {
|
||||||
|
type: String,
|
||||||
|
default: 'arrow-right',
|
||||||
|
},
|
||||||
colorScheme: {
|
colorScheme: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ import {
|
|||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
import {
|
||||||
|
replaceVariablesInMessage,
|
||||||
|
createTypingIndicator,
|
||||||
|
} from '@chatwoot/utils';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
@@ -140,6 +143,15 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
typingIndicator: createTypingIndicator(
|
||||||
|
() => {
|
||||||
|
this.$emit('typing-on');
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.$emit('typing-off');
|
||||||
|
},
|
||||||
|
TYPING_INDICATOR_IDLE_TIME
|
||||||
|
),
|
||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
showVariables: false,
|
showVariables: false,
|
||||||
@@ -638,15 +650,6 @@ export default {
|
|||||||
hideMentions() {
|
hideMentions() {
|
||||||
this.showUserMentions = false;
|
this.showUserMentions = false;
|
||||||
},
|
},
|
||||||
resetTyping() {
|
|
||||||
this.$emit('typing-off');
|
|
||||||
this.idleTimer = null;
|
|
||||||
},
|
|
||||||
turnOffIdleTimer() {
|
|
||||||
if (this.idleTimer) {
|
|
||||||
clearTimeout(this.idleTimer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleLineBreakWhenEnterToSendEnabled(event) {
|
handleLineBreakWhenEnterToSendEnabled(event) {
|
||||||
if (
|
if (
|
||||||
hasPressedEnterAndNotCmdOrShift(event) &&
|
hasPressedEnterAndNotCmdOrShift(event) &&
|
||||||
@@ -666,14 +669,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyup() {
|
onKeyup() {
|
||||||
if (!this.idleTimer) {
|
this.typingIndicator.start();
|
||||||
this.$emit('typing-on');
|
|
||||||
}
|
|
||||||
this.turnOffIdleTimer();
|
|
||||||
this.idleTimer = setTimeout(
|
|
||||||
() => this.resetTyping(),
|
|
||||||
TYPING_INDICATOR_IDLE_TIME
|
|
||||||
);
|
|
||||||
this.updateImgToolbarOnDelete();
|
this.updateImgToolbarOnDelete();
|
||||||
},
|
},
|
||||||
onKeydown(event) {
|
onKeydown(event) {
|
||||||
@@ -685,8 +681,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.turnOffIdleTimer();
|
this.typingIndicator.stop();
|
||||||
this.resetTyping();
|
|
||||||
this.$emit('blur');
|
this.$emit('blur');
|
||||||
},
|
},
|
||||||
onFocus() {
|
onFocus() {
|
||||||
|
|||||||
@@ -101,10 +101,10 @@
|
|||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<div
|
||||||
v-show="$refs.upload && $refs.upload.dropActive"
|
v-show="$refs.upload && $refs.upload.dropActive"
|
||||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
|
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="cloud-backup" size="40" />
|
<fluent-icon icon="cloud-backup" size="40" />
|
||||||
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
<h4 class="page-sub-title text-slate-900 dark:text-slate-50">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,6 +144,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|||||||
import {
|
import {
|
||||||
ALLOWED_FILE_TYPES,
|
ALLOWED_FILE_TYPES,
|
||||||
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
||||||
|
ALLOWED_FILE_TYPES_FOR_LINE,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/constants/messages';
|
||||||
import VideoCallButton from '../VideoCallButton.vue';
|
import VideoCallButton from '../VideoCallButton.vue';
|
||||||
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
||||||
@@ -270,6 +271,9 @@ export default {
|
|||||||
return this.showFileUpload || this.isNote;
|
return this.showFileUpload || this.isNote;
|
||||||
},
|
},
|
||||||
showAudioRecorderButton() {
|
showAudioRecorderButton() {
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Disable audio recorder for safari browser as recording is not supported
|
// Disable audio recorder for safari browser as recording is not supported
|
||||||
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
||||||
navigator.userAgent
|
navigator.userAgent
|
||||||
@@ -291,6 +295,9 @@ export default {
|
|||||||
if (this.isATwilioWhatsAppChannel) {
|
if (this.isATwilioWhatsAppChannel) {
|
||||||
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
|
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
|
||||||
}
|
}
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return ALLOWED_FILE_TYPES_FOR_LINE;
|
||||||
|
}
|
||||||
return ALLOWED_FILE_TYPES;
|
return ALLOWED_FILE_TYPES;
|
||||||
},
|
},
|
||||||
enableDragAndDrop() {
|
enableDragAndDrop() {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<mention-box :items="items" @mention-select="handleMentionClick">
|
<mention-box
|
||||||
|
v-if="items.length"
|
||||||
|
:items="items"
|
||||||
|
@mention-select="handleMentionClick"
|
||||||
|
>
|
||||||
<template slot-scope="{ item }">
|
<template slot-scope="{ item }">
|
||||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
<strong>{{ item.label }}</strong> - {{ item.description }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user