Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b96c7c60 | ||
|
|
6a9c885926 | ||
|
|
ed317c5bb3 | ||
|
|
319412f6dc | ||
|
|
0e4196c91a | ||
|
|
70d41ffcdd | ||
|
|
e09941db1c | ||
|
|
37bf4b7dd8 | ||
|
|
9e052fd5b2 | ||
|
|
dfcc33cbdd | ||
|
|
0f148f7700 | ||
|
|
acb39cbc8f | ||
|
|
d492a65c24 | ||
|
|
3d807684bb | ||
|
|
21d2b4d918 | ||
|
|
1f59205f20 | ||
|
|
a065165bcb | ||
|
|
4907489ea8 | ||
|
|
4d668d8db3 | ||
|
|
8daf1fe033 | ||
|
|
29f54c1f26 | ||
|
|
1a4faab381 | ||
|
|
c3314dd186 | ||
|
|
bdc4ecffc1 | ||
|
|
462214aced | ||
|
|
c523a953f7 | ||
|
|
c7482696d4 | ||
|
|
2035d24cb8 | ||
|
|
9c257578b0 | ||
|
|
b58ca21f0b | ||
|
|
edec74c389 | ||
|
|
f934a5eb04 | ||
|
|
f477c5281f | ||
|
|
6265c1c907 | ||
|
|
e834b545ef | ||
|
|
c37966744f | ||
|
|
ab54d9c629 | ||
|
|
7e960b7c72 | ||
|
|
92c14fa87d | ||
|
|
9b01b82cc7 | ||
|
|
0475060245 | ||
|
|
faf104c1fe | ||
|
|
d88e3e3596 | ||
|
|
9f3f238cb5 | ||
|
|
5b3339cd33 | ||
|
|
bfa00cc12d | ||
|
|
e46aa1aa64 | ||
|
|
2890339734 | ||
|
|
915366c472 | ||
|
|
f2b5e328bb | ||
|
|
672e5874fb | ||
|
|
8f30abb98b | ||
|
|
223385d134 | ||
|
|
7662fdce47 | ||
|
|
a47ca9cf4b | ||
|
|
b44f9b792b | ||
|
|
359c3c8ccb | ||
|
|
d7da6fbfb5 | ||
|
|
7664006625 | ||
|
|
6e1493501a | ||
|
|
7e0937f3ed | ||
|
|
d955d8e7dc | ||
|
|
4d45ac3bfc | ||
|
|
ecdf977de7 | ||
|
|
6b6df7a70d | ||
|
|
a7ca55c080 | ||
|
|
a0886d37bc | ||
|
|
caf8669db5 | ||
|
|
d1619bdc87 | ||
|
|
217dd8a6f0 | ||
|
|
a235e82a02 | ||
|
|
a2fca2edb8 | ||
|
|
9de36f2b9e | ||
|
|
fa3cc40059 | ||
|
|
3941c6f32b | ||
|
|
eda52930be | ||
|
|
5aac2acf56 | ||
|
|
85ea56b765 | ||
|
|
72069e45ef | ||
|
|
e0535168a0 | ||
|
|
b56512eb56 | ||
|
|
cf785123a5 | ||
|
|
aa7db90cd2 | ||
|
|
d7982a6ffd | ||
|
|
3d18ec9e40 | ||
|
|
a5bc81b304 | ||
|
|
dfddf9cacc | ||
|
|
cb44eb2964 | ||
|
|
b7806fc210 | ||
|
|
f9b55944ad | ||
|
|
fae8466a6c | ||
|
|
deb4513584 | ||
|
|
15085cfb98 | ||
|
|
fc4ef1595b | ||
|
|
ba547b3f60 | ||
|
|
5726447e47 | ||
|
|
a9ccded62b | ||
|
|
64e69a85f8 | ||
|
|
4a2195aeda | ||
|
|
efd6f735dc | ||
|
|
38cce3c1c0 | ||
|
|
e89e184d3b | ||
|
|
0d02142925 | ||
|
|
bbc47971d7 | ||
|
|
104ae8de2e | ||
|
|
e11b3c4f17 | ||
|
|
0bd48129b9 | ||
|
|
48127e00d7 | ||
|
|
abda86f9a7 | ||
|
|
8341a43143 | ||
|
|
f0f66c7da6 | ||
|
|
e6e9916fdb | ||
|
|
dd9d5e410c | ||
|
|
2e71006f9d | ||
|
|
30832d8a34 | ||
|
|
fa37f8e185 | ||
|
|
07a422219a | ||
|
|
8027c69c1c | ||
|
|
044b6872a4 | ||
|
|
b705eb39e2 | ||
|
|
ac657e3bf9 | ||
|
|
bed1979986 | ||
|
|
151bfbd1dd | ||
|
|
d840b7b13d | ||
|
|
bd1b17baf3 | ||
|
|
b769fc87d0 | ||
|
|
ad431c2008 | ||
|
|
dbddb1ece4 | ||
|
|
2f9637bde5 | ||
|
|
ae53aa6cfb | ||
|
|
cd11efea1d | ||
|
|
640e028bde | ||
|
|
bd9bac75ad | ||
|
|
29b9915d3b | ||
|
|
a357b657a8 | ||
|
|
3d748aa8d4 | ||
|
|
f809431074 | ||
|
|
e34fe1c614 | ||
|
|
e45abebe39 | ||
|
|
6c49e58ff8 | ||
|
|
50e4bb3e63 | ||
|
|
6cdefb19a2 |
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:2.7.2-node-browsers
|
||||
- image: circleci/ruby:3.0.2-node-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
||||
+1
-2
@@ -30,6 +30,5 @@ exclude_patterns:
|
||||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "stories/**/*"
|
||||
- "**/*.stories.js"
|
||||
- "**/stories/"
|
||||
- "stories/"
|
||||
|
||||
@@ -1,51 +1,8 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=2.7
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# 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 \
|
||||
groupmod --gid $USER_GID vscode \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||
fi
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
|
||||
# tmux is for overmind
|
||||
# TODO : install foreman in future
|
||||
# packages: postgresql-server-dev-all
|
||||
# may be postgres in same machine
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
libssl-dev \
|
||||
tar \
|
||||
tzdata \
|
||||
postgresql-client \
|
||||
yarn \
|
||||
git \
|
||||
imagemagick \
|
||||
tmux \
|
||||
zsh
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
|
||||
# The below image is created out of the Dockerfile.base
|
||||
# It has the dependencies already installed so that codespace will boot up fast
|
||||
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
|
||||
# TODO: figure out installing rvm
|
||||
# RUN rvm install
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN gem install bundler
|
||||
RUN bundle install
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
RUN yarn && gem install bundler && bundle install
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=3
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# 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 \
|
||||
groupmod --gid $USER_GID vscode \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||
fi
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
libssl-dev \
|
||||
tar \
|
||||
tzdata \
|
||||
postgresql-client \
|
||||
yarn \
|
||||
git \
|
||||
imagemagick \
|
||||
tmux \
|
||||
zsh
|
||||
|
||||
|
||||
# Install overmind
|
||||
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 \
|
||||
&& sudo mv overmind /usr/local/bin \
|
||||
&& chmod +x /usr/local/bin/overmind
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
RUN yarn
|
||||
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN gem install bundler && bundle install
|
||||
|
||||
@@ -12,22 +12,28 @@
|
||||
"extensions": [
|
||||
"rebornix.Ruby",
|
||||
"misogi.ruby-rubocop",
|
||||
"wingrunr21.vscode-ruby"
|
||||
"wingrunr21.vscode-ruby",
|
||||
"davidpallinder.rails-test-runner",
|
||||
"eamodio.gitlens",
|
||||
"github.copilot",
|
||||
"mrmlnc.vscode-duplicate"
|
||||
],
|
||||
|
||||
|
||||
// TODO: figure whether we can get all this ports work properly
|
||||
|
||||
// 3000 rails
|
||||
// 3035 webpacker
|
||||
// 5432 postgres
|
||||
// 6379 redis
|
||||
// 1025,8025 mailhog
|
||||
"forwardPorts": [5432, 6379, 1025, 8025],
|
||||
"forwardPorts": [8025],
|
||||
//your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to []
|
||||
"appPort": [3000, 3035],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// #TODO: can we move logic of copy env file into dockerfile ?
|
||||
"postCreateCommand": "cp .env.example .env",
|
||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Rails Server"
|
||||
},
|
||||
"8025": {
|
||||
"label": "Mailhog UI"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ services:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
|
||||
VARIANT: 2.7
|
||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
||||
VARIANT: 3
|
||||
# [Choice] Install Node.js
|
||||
INSTALL_NODE: "true"
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
cp .env.example .env
|
||||
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
|
||||
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
|
||||
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
|
||||
# uncomment the webpacker env variable
|
||||
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
|
||||
@@ -147,6 +147,10 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
# maxmindb api key to use geoip2 service
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=false
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
@@ -155,3 +159,9 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
ENABLE_PUSH_RELAY_SERVER=true
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Publish Codespace Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-code-space-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build the Codespace Base Image
|
||||
run: |
|
||||
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
|
||||
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
+36
-5
@@ -13,6 +13,7 @@ Metrics/ClassLength:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -24,12 +25,13 @@ Style/FrozenStringLiteralComment:
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
- 'app/dispatchers/dispatcher.rb'
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
- 'config/initializers/01_redis.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
Style/ClassVars:
|
||||
@@ -38,6 +40,14 @@ Style/ClassVars:
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'app/drops/base_drop.rb'
|
||||
Lint/SymbolConversion:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Exclude:
|
||||
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||
Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
@@ -55,6 +65,11 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/widgets_controller.rb'
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
@@ -64,6 +79,10 @@ RSpec/NestedGroups:
|
||||
Max: 4
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
@@ -77,7 +96,7 @@ Style/GuardClause:
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- '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'
|
||||
@@ -106,19 +125,24 @@ Rails/BulkChangeTable:
|
||||
- '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:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 12
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
@@ -133,4 +157,11 @@ AllCops:
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
- 'config/initializers/azure_storage_service_patch.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
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.7.2
|
||||
3.0.2
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
@@ -13,6 +14,7 @@ Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.component('multiselect', Multiselect);
|
||||
|
||||
const store = new Vuex.Store({});
|
||||
const i18nConfig = new VueI18n({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.7.2'
|
||||
ruby '3.0.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -33,19 +33,22 @@ gem 'liquid'
|
||||
gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down', '~> 5.0'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'mini_magick'
|
||||
gem 'image_processing'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
gem 'redis-rack-cache'
|
||||
# super fast record imports in bulk
|
||||
gem 'activerecord-import'
|
||||
|
||||
@@ -53,6 +56,7 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
@@ -89,8 +93,11 @@ gem 'google-cloud-dialogflow'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
gem 'ddtrace'
|
||||
gem 'scout_apm'
|
||||
gem 'sentry-raven'
|
||||
gem 'sentry-rails'
|
||||
gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
@@ -119,7 +126,7 @@ group :development do
|
||||
gem 'web-console'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: '131b11294fd6af9c428171f38516e6222a58c874'
|
||||
gem 'json_refs'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
@@ -133,19 +140,19 @@ group :test do
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem 'active_record_query_trace'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||
gem 'rspec-rails', '~> 5.0.0'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
gem 'scss_lint', require: false
|
||||
gem 'seed_dump'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
|
||||
+279
-255
@@ -1,81 +1,71 @@
|
||||
GIT
|
||||
remote: https://github.com/sds/mock_redis
|
||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
specs:
|
||||
mock_redis (0.22.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/tzmfreedom/json_refs
|
||||
revision: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
ref: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
specs:
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actioncable (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionmailbox (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
actionmailer (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
actionpack (6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actiontext (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionview (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord (6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord-import (1.0.7)
|
||||
activemodel (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activerecord (6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
marcel (~> 1.0.0)
|
||||
activesupport (6.0.3.7)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
addressable (2.7.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
acts-as-taggable-on (8.1.0)
|
||||
activerecord (>= 5.0, < 6.2)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
actionpack (>= 5.0)
|
||||
@@ -90,23 +80,23 @@ GEM
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.360.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.482.0)
|
||||
aws-sdk-core (3.119.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.37.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sdk-kms (1.46.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.79.1)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-sdk-s3 (1.98.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-sigv4 (1.2.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
@@ -120,42 +110,48 @@ GEM
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
barnes (0.0.8)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.8)
|
||||
bootsnap (1.7.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.9.0)
|
||||
brakeman (5.1.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
bullet (6.1.4)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.7.0.1)
|
||||
bundler-audit (0.8.0)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (>= 0.18, < 2)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
commonmarker (0.21.1)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.8)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
commonmarker (0.22.0)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.8.0)
|
||||
cypress-on-rails (1.10.1)
|
||||
rack
|
||||
database_cleaner (1.8.5)
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
database_cleaner-active_record (2.0.1)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.51.1)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.8.0)
|
||||
@@ -167,48 +163,49 @@ GEM
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
devise_token_auth (1.1.4)
|
||||
devise_token_auth (1.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.1)
|
||||
sprockets (= 3.7.2)
|
||||
rails (>= 4.2.0, < 6.2)
|
||||
diff-lcs (1.4.4)
|
||||
digest-crc (0.6.1)
|
||||
rake (~> 13.0)
|
||||
docile (1.3.2)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
ecma-re-validator (0.2.1)
|
||||
regexp_parser (~> 1.2)
|
||||
equalizer (0.0.11)
|
||||
down (5.2.3)
|
||||
addressable (~> 2.8)
|
||||
dry-inflector (0.2.1)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
execjs (2.7.0)
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.1.0)
|
||||
factory_bot (6.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.1.0)
|
||||
factory_bot (~> 6.1.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.13.0)
|
||||
faker (2.18.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.2)
|
||||
faraday (~> 1.0.0)
|
||||
ffi (1.15.0)
|
||||
fcm (1.0.3)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.3)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.4.1)
|
||||
fugit (1.5.0)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
@@ -217,18 +214,23 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.3)
|
||||
gli (2.19.2)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.43.0)
|
||||
geocoder (1.6.7)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.1)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.6.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.6.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
@@ -241,21 +243,23 @@ GEM
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-storage (1.28.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.15.8)
|
||||
google-protobuf (3.17.3-universal-darwin)
|
||||
google-protobuf (3.17.3-x86_64-linux)
|
||||
googleapis-common-protos (1.3.11)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.0.6)
|
||||
googleapis-common-protos-types (1.1.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.16.2)
|
||||
googleauth (0.17.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -264,11 +268,14 @@ GEM
|
||||
signet (~> 0.14)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.37.1)
|
||||
grpc (1.38.0-universal-darwin)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.0)
|
||||
hairtrigger (0.2.23)
|
||||
grpc (1.38.0-x86_64-linux)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
activerecord (>= 5.0, < 7)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
@@ -277,7 +284,7 @@ GEM
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.18.1)
|
||||
mime-types (~> 3.0)
|
||||
@@ -286,19 +293,23 @@ GEM
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
jbuilder (2.10.0)
|
||||
image_processing (1.12.1)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.2)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.1)
|
||||
json_schemer (0.2.16)
|
||||
ecma-re-validator (~> 0.2)
|
||||
json (2.5.1)
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
json_schemer (0.2.18)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 1.5)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
kaminari (1.2.1)
|
||||
@@ -321,11 +332,11 @@ GEM
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (4.0.3)
|
||||
listen (3.3.3)
|
||||
liquid (5.0.1)
|
||||
listen (3.6.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.9.1)
|
||||
loofah (2.11.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -336,97 +347,97 @@ GEM
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0225)
|
||||
mini_magick (4.10.1)
|
||||
mime-types-data (3.2021.0704)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.28.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
msgpack (1.4.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
net-http-persistent (4.0.0)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.6)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
parallel (1.19.2)
|
||||
parser (2.7.1.4)
|
||||
parallel (1.20.1)
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.15.0)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.13.1)
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (4.3.8)
|
||||
puma (5.4.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
rack (2.2.3)
|
||||
rack-cache (1.12.0)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.5.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.6.5)
|
||||
rack-proxy (0.7.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.3.7)
|
||||
actioncable (= 6.0.3.7)
|
||||
actionmailbox (= 6.0.3.7)
|
||||
actionmailer (= 6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actiontext (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.7)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.4)
|
||||
actioncable (= 6.1.4)
|
||||
actionmailbox (= 6.1.4)
|
||||
actionmailer (= 6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actiontext (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
railties (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.3)
|
||||
rb-fsevent (0.10.4)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.2.1)
|
||||
redis-namespace (1.8.0)
|
||||
redis (4.4.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack-cache (2.2.1)
|
||||
rack-cache (>= 1.10, < 2)
|
||||
redis-store (>= 1.6, < 2)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.1)
|
||||
representable (3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
@@ -438,57 +449,53 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
rspec-core (~> 3.9)
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.3)
|
||||
rubocop (0.89.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.1)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rspec-core (~> 3.10)
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.18.4)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.1)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 0.3.0, < 1.0)
|
||||
rubocop-ast (>= 1.8.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.3.0)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-performance (1.7.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop-rails (2.8.1)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.8.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.4)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.11.3)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.87.0)
|
||||
rubocop-rspec (1.43.2)
|
||||
rubocop (~> 0.87)
|
||||
ruby-enum (0.9.0)
|
||||
i18n
|
||||
ruby-progressbar (1.10.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.2)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.15.0)
|
||||
rubocop (>= 0.87.0)
|
||||
sexp_processor (~> 4.9)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_parser (3.16.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
@@ -497,21 +504,30 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (2.6.9)
|
||||
scout_apm (4.1.1)
|
||||
parser
|
||||
scss_lint (0.59.0)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (2.3.0)
|
||||
sentry-raven (3.0.3)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.6.4)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sentry-ruby (4.6.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sexp_processor (4.15.1)
|
||||
shoulda-matchers (4.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.1.1)
|
||||
sentry-ruby-core (= 4.6.4)
|
||||
sentry-ruby-core (4.6.4)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.6.4)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@@ -528,7 +544,7 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.15.0)
|
||||
slack-ruby-client (0.17.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
@@ -538,7 +554,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (3.7.2)
|
||||
sprockets (4.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
@@ -546,27 +562,28 @@ GEM
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.4.0)
|
||||
telegram-bot-ruby (0.12.0)
|
||||
statsd-ruby (1.5.0)
|
||||
telegram-bot-ruby (0.16.0)
|
||||
dry-inflector
|
||||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.9)
|
||||
virtus (~> 2.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.1)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
twitty (0.1.4)
|
||||
oauth
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
@@ -574,46 +591,49 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
unicode-display_width (2.0.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (3.3.1)
|
||||
valid_email2 (4.0.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
virtus (2.0.0)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.0.4)
|
||||
web-console (4.1.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.8.3)
|
||||
webmock (3.13.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.2.1)
|
||||
webpacker (5.4.0)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.3)
|
||||
webrick (1.7.0)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
arm64-darwin-20
|
||||
x86_64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
active_record_query_trace
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
@@ -631,10 +651,12 @@ DEPENDENCIES
|
||||
commonmarker
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
ddtrace
|
||||
devise
|
||||
devise-secure_password (~> 2.0)
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
@@ -648,8 +670,9 @@ DEPENDENCIES
|
||||
haikunator
|
||||
hairtrigger
|
||||
hashie
|
||||
image_processing
|
||||
jbuilder
|
||||
json_refs!
|
||||
json_refs
|
||||
json_schemer
|
||||
jwt
|
||||
kaminari
|
||||
@@ -658,29 +681,30 @@ DEPENDENCIES
|
||||
liquid
|
||||
listen
|
||||
maxminddb
|
||||
mini_magick
|
||||
mock_redis!
|
||||
mock_redis
|
||||
pg
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-attack
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
redis
|
||||
redis-namespace
|
||||
redis-rack-cache
|
||||
responders
|
||||
rest-client
|
||||
rspec-rails (~> 4.0.0.beta2)
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-raven
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
@@ -704,7 +728,7 @@ DEPENDENCIES
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.2p137
|
||||
ruby 3.0.2p107
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.2.25
|
||||
|
||||
@@ -29,9 +29,8 @@ ___
|
||||
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||
|
||||
@@ -57,28 +56,22 @@ Other features include:
|
||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
---
|
||||
## Documentation
|
||||
|
||||
### Documentation
|
||||
Detailed documentation is available at [chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||
|
||||
Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||
|
||||
### Translation process
|
||||
## Translation process
|
||||
|
||||
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
||||
|
||||
---
|
||||
|
||||
### Branching model
|
||||
## Branching model
|
||||
|
||||
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||
|
||||
---
|
||||
## Deployment
|
||||
|
||||
### Deployment
|
||||
|
||||
#### Heroku one-click deploy
|
||||
### Heroku one-click deploy
|
||||
|
||||
Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button:
|
||||
|
||||
@@ -86,13 +79,21 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
|
||||
|
||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||
|
||||
#### Other deployment options
|
||||
### Other deployment options
|
||||
|
||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||
|
||||
---
|
||||
## Security
|
||||
|
||||
### Contributors ✨
|
||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||
|
||||
|
||||
## Community? Questions? Support ?
|
||||
|
||||
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
||||
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
|
||||
+24
-1
@@ -1,8 +1,31 @@
|
||||
# Security Policy
|
||||
Chatwoot is looking forward to working with security researchers across the world to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
||||
|
||||
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||
|
||||
If you have any questions about the process, feel free to reach out to hello@chatwoot.com.
|
||||
If you have any questions about the process, feel free to reach out to security@chatwoot.com.
|
||||
|
||||
|
||||
## Out of scope
|
||||
|
||||
Please do not perform testing against Chatwoot production services. Use a self hosted instance to perform tests.
|
||||
|
||||
We consider the following to be out of scope, though there may be exceptions.
|
||||
|
||||
- Missing HTTP security headers
|
||||
- Self XSS
|
||||
- HTTP Host Header XSS without working proof-of-concept
|
||||
- Incomplete/Missing SPF/DKIM
|
||||
- Denial of Service attacks
|
||||
- DNSSEC
|
||||
- Social Engineering attacks
|
||||
|
||||
If you are not sure about the scope, please create a report.
|
||||
|
||||
## Thanks
|
||||
|
||||
Thank you for keeping Chatwoot and our users safe. 🙇
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
"FRONTEND_URL": {
|
||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||
"value": "https://CHANGE.herokuapp.com"
|
||||
},
|
||||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -3,8 +3,9 @@ class ContactIdentifyAction
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
merge_if_existing_identified_contact
|
||||
merge_if_existing_email_contact
|
||||
merge_if_existing_phone_number_contact
|
||||
update_contact
|
||||
end
|
||||
@contact
|
||||
@@ -16,6 +17,18 @@ class ContactIdentifyAction
|
||||
@account ||= @contact.account
|
||||
end
|
||||
|
||||
def merge_if_existing_identified_contact
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_email_contact
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_phone_number_contact
|
||||
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
|
||||
end
|
||||
|
||||
def existing_identified_contact
|
||||
return if params[:identifier].blank?
|
||||
|
||||
@@ -28,6 +41,12 @@ class ContactIdentifyAction
|
||||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def existing_phone_number_contact
|
||||
return if params[:phone_number].blank?
|
||||
|
||||
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def merge_contacts?(existing_contact, _contact)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
end
|
||||
@@ -36,7 +55,9 @@ class ContactIdentifyAction
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
# blank identifier or email will throw unique index error
|
||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
||||
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ class ContactMergeAction
|
||||
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
||||
|
||||
def perform
|
||||
# This case happens when an agent updates a contact email in dashboard,
|
||||
# while the contact also update his email via email collect box
|
||||
return @base_contact if base_contact.id == mergee_contact.id
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
validate_contacts
|
||||
merge_conversations
|
||||
|
||||
@@ -38,17 +38,30 @@ class ContactBuilder
|
||||
end
|
||||
|
||||
def find_contact
|
||||
contact = nil
|
||||
|
||||
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
|
||||
|
||||
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
|
||||
|
||||
contact ||= account.contacts.find_by(phone_number: contact_attributes[:phone_number]) if contact_attributes[:phone_number].present?
|
||||
|
||||
contact = find_contact_by_identifier(contact_attributes[:identifier])
|
||||
contact ||= find_contact_by_email(contact_attributes[:email])
|
||||
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
|
||||
contact
|
||||
end
|
||||
|
||||
def find_contact_by_identifier(identifier)
|
||||
return if identifier.blank?
|
||||
|
||||
account.contacts.find_by(identifier: identifier)
|
||||
end
|
||||
|
||||
def find_contact_by_email(email)
|
||||
return if email.blank?
|
||||
|
||||
account.contacts.find_by(email: email.downcase)
|
||||
end
|
||||
|
||||
def find_contact_by_phone_number(phone_number)
|
||||
return if phone_number.blank?
|
||||
|
||||
account.contacts.find_by(phone_number: phone_number)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = find_contact || create_contact
|
||||
@@ -57,6 +70,7 @@ class ContactBuilder
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class CsatSurveys::ResponseBuilder
|
||||
pattr_initialize [:message]
|
||||
|
||||
def perform
|
||||
raise 'Invalid Message' unless message.input_csat?
|
||||
|
||||
conversation = message.conversation
|
||||
rating = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'rating')
|
||||
feedback_message = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'feedback_message')
|
||||
|
||||
return if rating.blank?
|
||||
|
||||
process_csat_response(conversation, rating, feedback_message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_csat_response(conversation, rating, feedback_message)
|
||||
csat_survey_response = message.csat_survey_response || CsatSurveyResponse.new(
|
||||
message_id: message.id, account_id: message.account_id, conversation_id: message.conversation_id,
|
||||
contact_id: conversation.contact_id, assigned_agent: conversation.assignee
|
||||
)
|
||||
csat_survey_response.rating = rating
|
||||
csat_survey_response.feedback_message = feedback_message
|
||||
csat_survey_response.save!
|
||||
csat_survey_response
|
||||
end
|
||||
end
|
||||
@@ -17,12 +17,18 @@ class Messages::Facebook::MessageBuilder
|
||||
end
|
||||
|
||||
def perform
|
||||
# This channel might require reauthorization, may be owner might have changed the fb password
|
||||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
@@ -36,7 +42,6 @@ class Messages::Facebook::MessageBuilder
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
@@ -56,10 +61,21 @@ class Messages::Facebook::MessageBuilder
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
attachment_file = Down.download(
|
||||
file_url
|
||||
)
|
||||
attachment.file.attach(
|
||||
io: attachment_file,
|
||||
filename: attachment_file.original_filename,
|
||||
content_type: attachment_file.content_type
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_contact_avatar
|
||||
return if contact_params[:remote_avatar_url].blank?
|
||||
return if @contact.avatar.attached?
|
||||
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url])
|
||||
end
|
||||
|
||||
def conversation
|
||||
@@ -136,9 +152,12 @@ class Messages::Facebook::MessageBuilder
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@@ -83,10 +84,6 @@ class V2::ReportBuilder
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def range
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
@@ -101,11 +98,4 @@ class V2::ReportBuilder
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource&.is_a?(AgentBot)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
|
||||
@@ -69,39 +69,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
return [] if data.empty?
|
||||
|
||||
data.inject([]) do |result, page_detail|
|
||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id'])
|
||||
result << page_detail
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar(facebook_inbox, page_id)
|
||||
uri = get_avatar_url(page_id)
|
||||
|
||||
return unless uri
|
||||
|
||||
avatar_resource = LocalResource.new(uri)
|
||||
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
end
|
||||
|
||||
def get_avatar_url(page_id)
|
||||
begin
|
||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||
uri = URI.parse(url)
|
||||
tries = 3
|
||||
begin
|
||||
response = uri.open(redirect: false)
|
||||
rescue OpenURI::HTTPRedirect => e
|
||||
uri = e.uri # assigned from the "Location" response header
|
||||
retry if (tries -= 1).positive?
|
||||
raise
|
||||
end
|
||||
pic_url = response.base_uri.to_s
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
pic_url = nil
|
||||
end
|
||||
pic_url
|
||||
avatar_file = Down.download(
|
||||
"http://graph.facebook.com/#{page_id}/picture?type=large"
|
||||
)
|
||||
facebook_inbox.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,6 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id, trigger_rules: {})
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,26 +7,26 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
sort_on :last_activity_at, type: :datetime
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@contacts = fetch_contact_last_seen_at(resolved_contacts)
|
||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
||||
end
|
||||
|
||||
def search
|
||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
contacts = resolved_contacts.where(
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||
search: "%#{params[:q]}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contact_last_seen_at(contacts)
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
@@ -72,22 +72,31 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
def resolved_contacts
|
||||
@resolved_contacts ||= Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
return @resolved_contacts if @resolved_contacts
|
||||
|
||||
@resolved_contacts = Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||
@resolved_contacts
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def fetch_contact_last_seen_at(contacts)
|
||||
filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
|
||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
|
||||
contacts_with_conversation_count
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
@@ -99,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
@@ -113,6 +122,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||
end
|
||||
|
||||
def set_include_contact_inboxes
|
||||
@include_contact_inboxes = if params[:include_contact_inboxes].present?
|
||||
params[:include_contact_inboxes] == 'true'
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
@@ -49,8 +50,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@status = @conversation.save
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
@@ -73,6 +74,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
private
|
||||
|
||||
def set_conversation_status
|
||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = status
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
@@ -106,12 +113,16 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
status = { status: 'pending' } if status[:status] == 'bot'
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes
|
||||
additional_attributes: additional_attributes,
|
||||
snoozed_until: params[:snoozed_until]
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::BaseController
|
||||
include Sift
|
||||
include DateRangeHelper
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_csat_survey_responses, only: [:index, :metrics]
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :set_current_page_surveys, only: [:index]
|
||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||
|
||||
sort_on :created_at, type: :datetime
|
||||
|
||||
def index; end
|
||||
|
||||
def metrics
|
||||
@total_count = @csat_survey_responses.count
|
||||
@ratings_count = @csat_survey_responses.group(:rating).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_total_sent_messages_count
|
||||
@csat_messages = Current.account.messages.input_csat
|
||||
@csat_messages = @csat_messages.where(created_at: range) if range.present?
|
||||
@total_sent_messages_count = @csat_messages.count
|
||||
end
|
||||
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
@csat_survey_responses = @csat_survey_responses.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_custom_attributes_definitions, except: [:create]
|
||||
before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy]
|
||||
DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.create!(
|
||||
permitted_payload
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_attribute_definition.update!(permitted_payload)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_attribute_definition.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_custom_attributes_definitions
|
||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
|
||||
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_custom_attribute_definition
|
||||
@custom_attribute_definition = @custom_attribute_definitions.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:custom_attribute_definition).permit(
|
||||
:attribute_display_name,
|
||||
:attribute_display_type,
|
||||
:attribute_key,
|
||||
:attribute_model,
|
||||
:default_value
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :filter_type)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_custom_filters, except: [:create]
|
||||
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
|
||||
DEFAULT_FILTER_TYPE = 'conversation'.freeze
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_filter = current_user.custom_filters.create!(
|
||||
permitted_payload.merge(account_id: Current.account.id)
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_filter.update!(permitted_payload)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_filter.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_custom_filters
|
||||
@custom_filters = current_user.custom_filters.where(
|
||||
account_id: Current.account.id,
|
||||
filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_custom_filter
|
||||
@custom_filter = @custom_filters.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:custom_filter).permit(
|
||||
:name,
|
||||
:filter_type,
|
||||
query: {}
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :filter_type)
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -88,12 +88,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, channel:
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, :csat_survey_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled, :csat_survey_enabled,
|
||||
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [
|
||||
:website_url,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
include AuthHelper
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: [:create]
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||
twitter_consumer.consume
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
before_action :set_global_config
|
||||
|
||||
def create
|
||||
build_contact
|
||||
set_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if @contact.present?
|
||||
|
||||
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def set_token
|
||||
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||
{ created_at_ip: request.remote_ip }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +54,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value] }])
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }] }])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -3,13 +3,12 @@ class ApplicationController < ActionController::Base
|
||||
include Pundit
|
||||
include SwitchLocale
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
before_action :set_current_user, unless: :devise_controller?
|
||||
around_action :switch_locale
|
||||
around_action :handle_with_exception, unless: :devise_controller?
|
||||
|
||||
# after_action :verify_authorized
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
|
||||
private
|
||||
@@ -17,7 +16,7 @@ class ApplicationController < ActionController::Base
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module AuthHelper
|
||||
def send_auth_headers(user)
|
||||
data = user.create_new_auth_token
|
||||
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:'access-token']] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:'token-type']] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:client]] = data['client']
|
||||
response.headers[DeviseTokenAuth.headers_names[:expiry]] = data['expiry']
|
||||
response.headers[DeviseTokenAuth.headers_names[:uid]] = data['uid']
|
||||
|
||||
@@ -27,7 +27,13 @@ class Installation::OnboardingController < ApplicationController
|
||||
|
||||
def finish_onboarding
|
||||
::Redis::Alfred.delete(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
ChatwootHub.register_instance(onboarding_params) if onboarding_params[:subscribe_to_updates]
|
||||
return if onboarding_params[:subscribe_to_updates].blank?
|
||||
|
||||
ChatwootHub.register_instance(
|
||||
onboarding_params.dig(:user, :company),
|
||||
onboarding_params.dig(:user, :name),
|
||||
onboarding_params.dig(:user, :email)
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class PlatformController < ActionController::Base
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
class PlatformController < ActionController::API
|
||||
before_action :ensure_access_token
|
||||
before_action :set_platform_app
|
||||
before_action :set_resource, only: [:update, :show, :destroy]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
class Public::Api::V1::CsatSurveyController < PublicController
|
||||
before_action :set_conversation
|
||||
before_action :set_message
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked
|
||||
|
||||
@message.update!(message_update_params[:message])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
return if params[:id].blank?
|
||||
|
||||
@conversation = Conversation.find_by!(uuid: params[:id])
|
||||
end
|
||||
|
||||
def set_message
|
||||
@message = @conversation.messages.find_by!(content_type: 'input_csat')
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }] }])
|
||||
end
|
||||
|
||||
def check_csat_locked
|
||||
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
||||
def update
|
||||
@message.update!(message_update_params)
|
||||
rescue StandardError => e
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# TODO: we should switch to ActionController::API for the base classes
|
||||
# One of the specs is failing when I tried doing that, lets revisit in future
|
||||
class PublicController < ActionController::Base
|
||||
skip_before_action :verify_authenticity_token
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class Survey::ResponsesController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ class SwaggerController < ApplicationController
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||
else
|
||||
head 404
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged
|
||||
class WidgetsController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
before_action :set_web_widget
|
||||
|
||||
@@ -10,10 +10,12 @@ class AsyncDispatcher < BaseDispatcher
|
||||
|
||||
def listeners
|
||||
[
|
||||
CampaignListener.instance,
|
||||
CsatSurveyListener.instance,
|
||||
EventListener.instance,
|
||||
WebhookListener.instance,
|
||||
InstallationWebhookListener.instance, HookListener.instance,
|
||||
CampaignListener.instance
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
WebhookListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,6 +21,27 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def perform
|
||||
set_up
|
||||
|
||||
mine_count, unassigned_count, all_count, = set_count_for_all_conversations
|
||||
assigned_count = all_count - unassigned_count
|
||||
|
||||
filter_by_assignee_type
|
||||
|
||||
{
|
||||
conversations: conversations,
|
||||
count: {
|
||||
mine_count: mine_count,
|
||||
assigned_count: assigned_count,
|
||||
unassigned_count: unassigned_count,
|
||||
all_count: all_count
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_up
|
||||
set_inboxes
|
||||
set_team
|
||||
set_assignee_type
|
||||
@@ -30,23 +51,8 @@ class ConversationFinder
|
||||
filter_by_team if @team
|
||||
filter_by_labels if params[:labels]
|
||||
filter_by_query if params[:q]
|
||||
|
||||
mine_count, unassigned_count, all_count = set_count_for_all_conversations
|
||||
|
||||
filter_by_assignee_type
|
||||
|
||||
{
|
||||
conversations: conversations,
|
||||
count: {
|
||||
mine_count: mine_count,
|
||||
unassigned_count: unassigned_count,
|
||||
all_count: all_count
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_inboxes
|
||||
@inbox_ids = if params[:inbox_id]
|
||||
current_account.inboxes.where(id: params[:inbox_id])
|
||||
@@ -73,6 +79,8 @@ class ConversationFinder
|
||||
@conversations = @conversations.assigned_to(current_user)
|
||||
when 'unassigned'
|
||||
@conversations = @conversations.unassigned
|
||||
when 'assigned'
|
||||
@conversations = @conversations.assigned
|
||||
end
|
||||
@conversations
|
||||
end
|
||||
@@ -106,13 +114,13 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page]
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def conversations
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
|
||||
)
|
||||
current_page ? @conversations.latest.page(current_page) : @conversations.latest
|
||||
@conversations.latest.page(current_page)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
##############################################
|
||||
# Helpers to implement date range filtering to APIs
|
||||
# Include in your controller or service class where params is available
|
||||
##############################################
|
||||
|
||||
module DateRangeHelper
|
||||
def range
|
||||
return if params[:since].blank? || params[:until].blank?
|
||||
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
module MessageFormatHelper
|
||||
include RegexHelper
|
||||
def transform_user_mention_content(message_content)
|
||||
message_content.gsub(MENTION_REGEX, '\1')
|
||||
end
|
||||
end
|
||||
@@ -3,12 +3,17 @@
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
<add-account-modal
|
||||
:show="showAddAccountModal"
|
||||
:has-accounts="hasAccounts"
|
||||
/>
|
||||
<woot-snackbar-box />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
@@ -17,14 +22,36 @@ export default {
|
||||
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
hasAccounts() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.accounts &&
|
||||
this.currentUser.accounts.length !== 0
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentUser() {
|
||||
if (!this.hasAccounts) {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
export const buildContactParams = (page, sortAttr, label, search) => {
|
||||
let params = `include_contact_inboxes=false&page=${page}&sort=${sortAttr}`;
|
||||
if (search) {
|
||||
params = `${params}&q=${search}`;
|
||||
}
|
||||
if (label) {
|
||||
params = `${params}&labels[]=${label}`;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
class ContactAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('contacts', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page, sortAttr = 'name') {
|
||||
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
|
||||
get(page, sortAttr = 'name', label = '') {
|
||||
let requestURL = `${this.url}?${buildContactParams(
|
||||
page,
|
||||
sortAttr,
|
||||
label,
|
||||
''
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
getConversations(contactId) {
|
||||
@@ -26,10 +43,14 @@ class ContactAPI extends ApiClient {
|
||||
return axios.post(`${this.url}/${contactId}/labels`, { labels });
|
||||
}
|
||||
|
||||
search(search = '', page = 1, sortAttr = 'name') {
|
||||
return axios.get(
|
||||
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
|
||||
);
|
||||
search(search = '', page = 1, sortAttr = 'name', label = '') {
|
||||
let requestURL = `${this.url}/search?${buildContactParams(
|
||||
page,
|
||||
sortAttr,
|
||||
label,
|
||||
search
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CSATReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, since: from, until: to, sort: '-created_at' },
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to } = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CSATReportsAPI();
|
||||
@@ -28,9 +28,10 @@ class ConversationApi extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus({ conversationId, status }) {
|
||||
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||
status,
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
export const buildCreatePayload = ({
|
||||
message,
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
payload = new FormData();
|
||||
payload.append('attachments[]', file, file.name);
|
||||
if (message) {
|
||||
payload.append('content', message);
|
||||
}
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
class MessageApi extends ApiClient {
|
||||
constructor() {
|
||||
super('conversations', { accountScoped: true });
|
||||
@@ -15,21 +42,23 @@ class MessageApi extends ApiClient {
|
||||
echo_id: echoId,
|
||||
file,
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
if (file) formData.append('attachments[]', file, file.name);
|
||||
if (message) formData.append('content', message);
|
||||
if (contentAttributes)
|
||||
formData.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
|
||||
formData.append('private', isPrivate);
|
||||
formData.append('echo_id', echoId);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
data: formData,
|
||||
data: buildCreatePayload({
|
||||
message,
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
delete(conversationID, messageId) {
|
||||
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
||||
}
|
||||
|
||||
getPreviousMessages({ conversationId, before }) {
|
||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||
params: { before },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import contactAPI from '../contacts';
|
||||
import contactAPI, { buildContactParams } from '../contacts';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
@@ -15,9 +15,9 @@ describe('#ContactsAPI', () => {
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name');
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts?page=1&sort=name'
|
||||
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -54,10 +54,26 @@ describe('#ContactsAPI', () => {
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
contactAPI.search('leads', 1, 'date');
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/search?q=leads&page=1&sort=date'
|
||||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buildContactParams', () => {
|
||||
it('returns correct string', () => {
|
||||
expect(buildContactParams(1, 'name', '', '')).toBe(
|
||||
'include_contact_inboxes=false&page=1&sort=name'
|
||||
);
|
||||
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
||||
'include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
expect(
|
||||
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
||||
).toBe(
|
||||
'include_contact_inboxes=false&page=1&sort=name&q=message-content&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import csatReportsAPI from '../csatReports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(csatReportsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(csatReportsAPI.apiVersion).toBe('/api/v1');
|
||||
expect(csatReportsAPI).toHaveProperty('get');
|
||||
expect(csatReportsAPI).toHaveProperty('getMetrics');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#get', () => {
|
||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses',
|
||||
{
|
||||
params: {
|
||||
page: 1,
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
sort: '-created_at',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/metrics',
|
||||
{
|
||||
params: { since: 1622485800, until: 1623695400 },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,7 @@ describe('#ConversationAPI', () => {
|
||||
`/api/v1/conversations/12/toggle_status`,
|
||||
{
|
||||
status: 'online',
|
||||
snoozed_until: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import messageAPI from '../../inbox/message';
|
||||
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
@@ -29,4 +29,34 @@ describe('#ConversationAPI', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('#buildCreatePayload', () => {
|
||||
it('builds form payload if file is available', () => {
|
||||
const formPayload = buildCreatePayload({
|
||||
message: 'test content',
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
file: new Blob(['test-content'], { type: 'application/pdf' }),
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
expect(formPayload.get('content')).toEqual('test content');
|
||||
expect(formPayload.get('echo_id')).toEqual('12');
|
||||
expect(formPayload.get('private')).toEqual('true');
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
expect(
|
||||
buildCreatePayload({
|
||||
message: 'test content',
|
||||
isPrivate: false,
|
||||
echoId: 12,
|
||||
contentAttributes: { in_reply_to: 12 },
|
||||
})
|
||||
).toEqual({
|
||||
content: 'test content',
|
||||
private: false,
|
||||
echo_id: 12,
|
||||
content_attributes: { in_reply_to: 12 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,29 @@
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.mx-datepicker-popup {
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-normal);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
height: 4.6rem;
|
||||
}
|
||||
|
||||
.mx-input:disabled,
|
||||
.mx-input[readonly] {
|
||||
background-color: var(--white);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,14 @@ code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
// remove when grid gutters are fixed
|
||||
.columns.with-right-space {
|
||||
padding-right: var(--space-normal);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
}
|
||||
|
||||
.sidebar-labels-wrap {
|
||||
|
||||
&.has-edited,
|
||||
&:hover {
|
||||
.multiselect {
|
||||
@@ -162,6 +163,12 @@
|
||||
.multiselect-wrap--small {
|
||||
$multiselect-height: 3.8rem;
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@@ -177,15 +184,28 @@
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
padding: var(--space-small) 0;
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
padding: var(--space-small) 0;
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@import 'variables';
|
||||
|
||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||
@import '~vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@@ -27,3 +28,5 @@
|
||||
@import 'foundation-custom';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/forms';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
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 {
|
||||
@@ -20,17 +36,4 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.update-subscription--checkbox {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
line-height: 1.5;
|
||||
margin-right: var(--space-one);
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
.channel {
|
||||
@include flex;
|
||||
@include padding($space-normal $zero);
|
||||
@include margin($zero);
|
||||
@include background-white;
|
||||
@include border-light;
|
||||
flex-direction: column;
|
||||
|
||||
cursor: pointer;
|
||||
border-right-color: $color-white;
|
||||
flex-direction: column;
|
||||
margin: -1px;
|
||||
transition: all 0.200s ease-in;
|
||||
|
||||
&:last-child {
|
||||
@@ -32,14 +32,13 @@
|
||||
|
||||
img {
|
||||
@include margin($space-normal auto);
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.channel__title{
|
||||
font-size: $font-size-large;
|
||||
text-align: center;
|
||||
.channel__title {
|
||||
color: $color-body;
|
||||
font-size: var(--font-size-default);
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ $default-button-height: 4.0rem;
|
||||
height: $default-button-height;
|
||||
margin-bottom: 0;
|
||||
|
||||
.button__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
padding: 0 var(--space-small);
|
||||
}
|
||||
|
||||
@@ -90,11 +90,6 @@
|
||||
font-size: $font-size-mini;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.message-from-agent {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-mini;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--meta {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.report-card {
|
||||
@include padding($space-normal $space-small $space-normal $space-two);
|
||||
@include margin($zero);
|
||||
@include background-light;
|
||||
cursor: pointer;
|
||||
@include custom-border-top(3px, transparent);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{ $t('CHAT_LIST.LIST.404') }}
|
||||
</p>
|
||||
|
||||
<div class="conversations-list">
|
||||
<div ref="activeConversation" class="conversations-list">
|
||||
<conversation-card
|
||||
v-for="chat in conversationList"
|
||||
:key="chat.id"
|
||||
@@ -62,8 +62,13 @@ import ChatFilter from './widgets/conversation/ChatFilter';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||
import timeMixin from '../mixins/time';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from '../constants';
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
hasPressedAltAndKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -71,7 +76,7 @@ export default {
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
},
|
||||
mixins: [timeMixin, conversationMixin],
|
||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||
props: {
|
||||
conversationInbox: {
|
||||
type: [String, Number],
|
||||
@@ -94,6 +99,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
@@ -188,6 +194,33 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
);
|
||||
const activeConversation = this.$refs.activeConversation.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
}
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else-if="isBot"
|
||||
v-else-if="showOpenButton"
|
||||
class-names="resolve"
|
||||
color-scheme="primary"
|
||||
icon="ion-person"
|
||||
@@ -34,7 +34,8 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showDropDown"
|
||||
v-if="showAdditionalActions"
|
||||
ref="arrowDownButton"
|
||||
:color-scheme="buttonClass"
|
||||
:disabled="isLoading"
|
||||
icon="ion-arrow-down-b"
|
||||
@@ -43,19 +44,53 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item v-if="!isBot">
|
||||
<woot-dropdown-item v-if="!isPending">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@click="() => toggleStatus(STATUS_TYPE.BOT)"
|
||||
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.OPEN_BOT') }}
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
|
||||
<woot-dropdown-sub-menu
|
||||
v-if="isOpen"
|
||||
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
|
||||
>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
|
||||
"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TOMORROW') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
|
||||
"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_WEEK') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-sub-menu>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,22 +100,37 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedAltAndEKey,
|
||||
hasPressedCommandPlusAltAndEKey,
|
||||
hasPressedAltAndMKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import wootConstants from '../../constants';
|
||||
import {
|
||||
getUnixTime,
|
||||
addHours,
|
||||
addWeeks,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
} from 'date-fns';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownSubMenu,
|
||||
},
|
||||
mixins: [clickaway, alertMixin],
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showDropdown: false,
|
||||
showActionsDropdown: false,
|
||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||
};
|
||||
},
|
||||
@@ -91,36 +141,83 @@ export default {
|
||||
isOpen() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
isBot() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.BOT;
|
||||
isPending() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.PENDING;
|
||||
},
|
||||
isResolved() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||
},
|
||||
isSnoozed() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
},
|
||||
buttonClass() {
|
||||
if (this.isBot) return 'primary';
|
||||
if (this.isPending) return 'primary';
|
||||
if (this.isOpen) return 'success';
|
||||
if (this.isResolved) return 'warning';
|
||||
return '';
|
||||
},
|
||||
showDropDown() {
|
||||
return !this.isBot;
|
||||
showAdditionalActions() {
|
||||
return !this.isPending && !this.isSnoozed;
|
||||
},
|
||||
snoozeTimes() {
|
||||
return {
|
||||
// tomorrow = 9AM next day
|
||||
tomorrow: getUnixTime(addHours(startOfTomorrow(), 9)),
|
||||
// next week = 9AM Monday, next week
|
||||
nextWeek: getUnixTime(
|
||||
addHours(startOfWeek(addWeeks(new Date(), 1), { weekStartsOn: 1 }), 9)
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleKeyEvents(e) {
|
||||
const allConversations = document.querySelectorAll(
|
||||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||
if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
} else if (allConversations.length > 1) {
|
||||
allConversations[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showOpenButton() {
|
||||
return this.isResolved || this.isSnoozed;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.showDropdown = true;
|
||||
this.showActionsDropdown = true;
|
||||
},
|
||||
toggleStatus(status) {
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.closeDropdown();
|
||||
this.isLoading = true;
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat.id,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint no-plusplus: 0 */
|
||||
/* eslint-env browser */
|
||||
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
|
||||
import Bar from './widgets/chart/BarChart';
|
||||
import Button from './ui/WootButton';
|
||||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import Label from './ui/Label';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
@@ -28,12 +28,14 @@ const WootUIKit = {
|
||||
Button,
|
||||
Code,
|
||||
ColorPicker,
|
||||
ConfirmDeleteModal,
|
||||
DeleteModal,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
HorizontalBar,
|
||||
Input,
|
||||
LoadingState,
|
||||
Label,
|
||||
LoadingState,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ReportStatsCard,
|
||||
@@ -43,7 +45,6 @@ const WootUIKit = {
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmDeleteModal,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
v-if="shouldShowSidebarItem"
|
||||
:key="labelSection.toState"
|
||||
:menu-item="labelSection"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
<sidebar-item
|
||||
v-if="showShowContactSideMenu"
|
||||
:key="contactLabelSection.key"
|
||||
:menu-item="contactLabelSection"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -43,10 +50,17 @@
|
||||
:show="showOptionsMenu"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@show-support-chat-window="toggleSupportChatWindow"
|
||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close="toggleOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@@ -57,6 +71,10 @@
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -74,6 +92,10 @@ import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
||||
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||
import { hasPressedCommandAndForwardSlash } from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -84,13 +106,17 @@ export default {
|
||||
OptionsMenu,
|
||||
AccountSelector,
|
||||
AddAccountModal,
|
||||
AddLabelModal,
|
||||
WootKeyShortcutModal,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin],
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -131,6 +157,9 @@ export default {
|
||||
shouldShowSidebarItem() {
|
||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
},
|
||||
showShowContactSideMenu() {
|
||||
return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
|
||||
},
|
||||
shouldShowTeams() {
|
||||
return this.shouldShowSidebarItem && this.teams.length;
|
||||
},
|
||||
@@ -177,6 +206,29 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactLabelSection() {
|
||||
return {
|
||||
icon: 'ion-pound',
|
||||
label: 'TAGGED_WITH',
|
||||
hasSubMenu: true,
|
||||
key: 'label',
|
||||
newLink: false,
|
||||
cssClass: 'menu-title align-justify',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
children: this.accountLabels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
color: label.color,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/labels/${label.title}/contacts`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
teamSection() {
|
||||
return {
|
||||
icon: 'ion-ios-people',
|
||||
@@ -214,7 +266,19 @@ export default {
|
||||
this.$store.dispatch('teams/get');
|
||||
this.setChatwootUser();
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
@@ -253,6 +317,12 @@ export default {
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabelModal = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -52,11 +52,6 @@
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
<add-label-modal
|
||||
v-if="showAddLabel"
|
||||
:show.sync="showAddLabel"
|
||||
:on-close="hideAddLabelPopup"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@@ -64,19 +59,17 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
export default {
|
||||
components: {
|
||||
AddLabelModal,
|
||||
},
|
||||
mixins: [adminMixin],
|
||||
data() {
|
||||
return {
|
||||
showAddLabel: false,
|
||||
};
|
||||
},
|
||||
mixins: [adminMixin, eventListenerMixins],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -127,19 +120,27 @@ export default {
|
||||
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
||||
} else if (item.showModalForNewItem) {
|
||||
if (item.modalName === 'AddLabel') {
|
||||
this.showAddLabelPopup();
|
||||
this.$emit('add-label');
|
||||
}
|
||||
}
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
if (hasPressedAltAndVKey(e)) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
if (hasPressedAltAndRKey(e)) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
if (hasPressedAltAndSKey(e)) {
|
||||
router.push({ name: 'settings_home' });
|
||||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabel = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabel = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div v-if="!hasAccounts" class="alert-wrap">
|
||||
<div class="callout alert">
|
||||
<div class="icon-wrap">
|
||||
<i class="ion-alert-circled"></i>
|
||||
</div>
|
||||
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="row" @submit.prevent="addAccount">
|
||||
<div class="medium-12 columns">
|
||||
@@ -53,6 +61,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasAccounts: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -90,3 +102,19 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.alert-wrap {
|
||||
margin: var(--space-zero) var(--space-large);
|
||||
margin-top: var(--space-medium);
|
||||
.callout {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
font-size: var(--font-size-big);
|
||||
margin-left: var(--space-smaller);
|
||||
margin-right: var(--space-slab);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,16 @@
|
||||
Contact Support
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
class=" change-accounts--button"
|
||||
@click="$emit('key-shortcut-modal')"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<router-link
|
||||
:to="`/app/accounts/${accountId}/profile/settings`"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="date-picker">
|
||||
<date-picker
|
||||
:range="true"
|
||||
:confirm="true"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:confirm-text="confirmText"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatePicker from 'vue2-datepicker';
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
props: {
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit('change', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="date-picker">
|
||||
<date-picker
|
||||
type="datetime"
|
||||
:confirm="true"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:confirm-text="confirmText"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
:disabled-date="disableBeforeToday"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import addDays from 'date-fns/addDays';
|
||||
import DatePicker from 'vue2-datepicker';
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
props: {
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Date,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit('change', value);
|
||||
},
|
||||
disableBeforeToday(date) {
|
||||
const yesterdayDate = addDays(new Date(), -1);
|
||||
return date < yesterdayDate;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import WootDateRangePicker from '../DateRangePicker.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Date Picker/Date Range Picker',
|
||||
argTypes: {
|
||||
confirmText: {
|
||||
defaultValue: 'Apply',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
defaultValue: 'Select date range',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { WootDateRangePicker },
|
||||
template:
|
||||
'<woot-date-range-picker v-bind="$props" @change="onChange"></woot-date-range-picker>',
|
||||
});
|
||||
|
||||
export const DateRangePicker = Template.bind({});
|
||||
DateRangePicker.args = {
|
||||
onChange: action('applied'),
|
||||
value: new Date(),
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import WootDateTimePicker from '../DateTimePicker.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Date Picker/Date Time Picker',
|
||||
argTypes: {
|
||||
confirmText: {
|
||||
defaultValue: 'Apply',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
defaultValue: 'Select date time',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { WootDateTimePicker },
|
||||
template:
|
||||
'<woot-date-time-picker v-bind="$props" @change="onChange"></woot-date-time-picker>',
|
||||
});
|
||||
|
||||
export const DateTimePicker = Template.bind({});
|
||||
DateTimePicker.args = {
|
||||
onChange: action('applied'),
|
||||
value: new Date(),
|
||||
};
|
||||
@@ -65,7 +65,11 @@ export default {
|
||||
display: flex;
|
||||
padding: var(--space-slab) 0 0;
|
||||
background: var(--color-background-light);
|
||||
background: transparent;
|
||||
background: var(--b-50);
|
||||
border-radius: var(--border-radius-normal);
|
||||
width: fit-content;
|
||||
padding: var(--space-smaller);
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.thumb-wrap {
|
||||
@@ -114,6 +118,8 @@ export default {
|
||||
max-width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: var(--space-small);
|
||||
|
||||
.item {
|
||||
height: var(--space-normal);
|
||||
overflow: hidden;
|
||||
@@ -123,7 +129,7 @@ export default {
|
||||
}
|
||||
|
||||
.file-size-wrap {
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="small-3 columns channel"
|
||||
class="small-6 medium-4 large-3 columns channel"
|
||||
:class="{ inactive: !isActive }"
|
||||
@click="onItemClick"
|
||||
>
|
||||
@@ -37,8 +37,12 @@
|
||||
src="~dashboard/assets/images/channels/website.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'twilio'"
|
||||
src="~dashboard/assets/images/channels/twilio.png"
|
||||
v-if="channel.key === 'sms'"
|
||||
src="~dashboard/assets/images/channels/sms.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'whatsapp'"
|
||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||
/>
|
||||
<h3 class="channel__title">
|
||||
{{ channel.name }}
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
if (key === 'email') {
|
||||
return this.enabledFeatures.channel_email;
|
||||
}
|
||||
return ['website', 'twilio', 'api'].includes(key);
|
||||
return ['website', 'twilio', 'api', 'whatsapp', 'sms'].includes(key);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
</template>
|
||||
<script>
|
||||
import wootConstants from '../../constants';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -28,6 +31,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndNKey(e)) {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="inbox">
|
||||
<i :class="icon" />
|
||||
<span>{{ inbox.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
export default {
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.inbox.channel_type === INBOX_TYPES.WEB) {
|
||||
return 'icon ion-earth';
|
||||
}
|
||||
return 'icon ion-android-textsms';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.inbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
margin-right: var(--space-micro);
|
||||
min-width: var(--space-normal);
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div class="small-2 report-card" :class="{ 'active': selected }" v-on:click="onClick(index)">
|
||||
<h3 class="heading">{{heading}}</h3>
|
||||
<h4 class="metric">{{point}}</h4>
|
||||
<p class="desc">{{desc}}</p>
|
||||
<div
|
||||
class="small-2 report-card"
|
||||
:class="{ active: selected }"
|
||||
@click="onClick(index)"
|
||||
>
|
||||
<h3 class="heading">
|
||||
{{ heading }}
|
||||
</h3>
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<p class="desc">
|
||||
{{ desc }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: String,
|
||||
|
||||
+14
-13
@@ -1,36 +1,37 @@
|
||||
<template>
|
||||
<div class="row--user-block">
|
||||
<Thumbnail
|
||||
:src="sender.thumbnail"
|
||||
size="20px"
|
||||
:username="sender.name"
|
||||
:status="sender.availability_status"
|
||||
<thumbnail
|
||||
:src="user.thumbnail"
|
||||
:size="size"
|
||||
:username="user.name"
|
||||
:status="user.availability_status"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-block-title text-truncate">
|
||||
{{ sender.name }}
|
||||
</h6>
|
||||
</div>
|
||||
<h6 class="text-block-title text-truncate text-capitalize">
|
||||
{{ user.name }}
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
sender: {
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '20px',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.row--user-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -57,6 +57,7 @@ export default {
|
||||
value: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
isPrivate: { type: Boolean, default: false },
|
||||
isFormatMode: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -139,8 +140,17 @@ export default {
|
||||
value(newValue = '') {
|
||||
if (newValue !== this.lastValue) {
|
||||
const { tr } = this.state;
|
||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||
this.state = this.view.state.apply(tr);
|
||||
if (this.isFormatMode) {
|
||||
this.state = createState(
|
||||
newValue,
|
||||
this.placeholder,
|
||||
this.plugins,
|
||||
this.isFormatMode
|
||||
);
|
||||
} else {
|
||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||
this.state = this.view.state.apply(tr);
|
||||
}
|
||||
this.view.updateState(this.state);
|
||||
}
|
||||
},
|
||||
@@ -271,4 +281,25 @@ export default {
|
||||
padding: 0 var(--space-smaller);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-wrap {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.message-editor {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-normal);
|
||||
padding: 0 var(--space-slab);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor_warning {
|
||||
border: 1px solid var(--r-400);
|
||||
}
|
||||
|
||||
.editor-warning__message {
|
||||
color: var(--r-400);
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: var(--space-smaller) 0 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
/>
|
||||
|
||||
<file-upload
|
||||
ref="upload"
|
||||
:size="4096 * 4096"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<woot-button
|
||||
@@ -37,6 +40,17 @@
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
class="modal-mask"
|
||||
>
|
||||
<i class="ion-ios-cloud-upload-outline icon"></i>
|
||||
<h4 class="page-sub-title">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div v-if="isFormatMode" class="enter-to-send--checkbox">
|
||||
@@ -64,11 +78,17 @@
|
||||
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import {
|
||||
hasPressedAltAndWKey,
|
||||
hasPressedAltAndAKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: { FileUpload },
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -142,6 +162,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndWKey(e)) {
|
||||
this.toggleFormatMode();
|
||||
}
|
||||
if (hasPressedAltAndAKey(e)) {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
}
|
||||
},
|
||||
toggleFormatMode() {
|
||||
this.setFormatMode(!this.isFormatMode);
|
||||
},
|
||||
@@ -198,4 +226,18 @@ export default {
|
||||
background: var(--s-100);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
color: var(--s-600);
|
||||
background: var(--white-transparent);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-sub-title {
|
||||
color: var(--s-600);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,11 +32,17 @@
|
||||
<script>
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
import {
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -76,6 +82,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.handleNoteClick();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.handleReplyClick();
|
||||
}
|
||||
},
|
||||
handleReplyClick() {
|
||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HorizontalBar } from 'vue-chartjs';
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
offsetGridLines: false,
|
||||
},
|
||||
display: false,
|
||||
stacked: true,
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
offsetGridLines: false,
|
||||
},
|
||||
display: false,
|
||||
stacked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
extends: HorizontalBar,
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.collection, {
|
||||
...chartOptions,
|
||||
...this.chartOptions,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -12,12 +12,29 @@
|
||||
|
||||
<script>
|
||||
import wootConstants from '../../../constants';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
data: () => ({
|
||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||
}),
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndBKey(e)) {
|
||||
if (this.activeStatus === wootConstants.STATUS_TYPE.OPEN) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.RESOLVED;
|
||||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.RESOLVED) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.PENDING;
|
||||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED;
|
||||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.OPEN;
|
||||
}
|
||||
}
|
||||
this.onTabChange();
|
||||
},
|
||||
onTabChange() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
this.$emit('statusFilterChange', this.activeStatus);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user