Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bfc2da19a | ||
|
|
6a1eb799a4 | ||
|
|
2ad9cd2055 | ||
|
|
ebae547a60 | ||
|
|
cc47ccaa2c | ||
|
|
d53097f77d | ||
|
|
9911c5dc12 | ||
|
|
19aef3e94b | ||
|
|
23230e0143 | ||
|
|
f92cea144c | ||
|
|
e6cf8c39b7 | ||
|
|
c5c0845151 | ||
|
|
5d9fb55370 | ||
|
|
721a2f5052 | ||
|
|
7320957405 | ||
|
|
e4d072c79c | ||
|
|
71ee10c889 | ||
|
|
978a8a4cb2 | ||
|
|
cd06b2b337 | ||
|
|
6eb06377cc | ||
|
|
94892e7168 | ||
|
|
fe744abe26 | ||
|
|
a902b49bc5 | ||
|
|
9c07b6dd46 | ||
|
|
3b7b06dbec | ||
|
|
597a4164e8 | ||
|
|
678c00f254 | ||
|
|
7298002da7 | ||
|
|
96b781b7fd | ||
|
|
1ce5cbe275 | ||
|
|
c607f09be0 | ||
|
|
33a6ad9f7e | ||
|
|
e662336eaf | ||
|
|
0158a55845 | ||
|
|
657843960c | ||
|
|
fc6a22b072 | ||
|
|
227d99934e | ||
|
|
5036b28e45 | ||
|
|
8b5735c915 | ||
|
|
eb379e1849 | ||
|
|
6a630bc489 | ||
|
|
0d1b474e60 | ||
|
|
84f6cadae7 | ||
|
|
372e740c38 | ||
|
|
57dd979a14 | ||
|
|
c1d07a5471 | ||
|
|
98eddd0532 | ||
|
|
1b21e0d429 | ||
|
|
7776b74126 | ||
|
|
3abb887445 | ||
|
|
a9ec3ae098 | ||
|
|
53927b5a72 | ||
|
|
d67b91d2b0 | ||
|
|
a4fc28abc0 | ||
|
|
bee2a14620 | ||
|
|
168a4dc323 | ||
|
|
47c0decf1e | ||
|
|
8acc818f68 | ||
|
|
9e0468cd73 | ||
|
|
65e9cee019 | ||
|
|
bb3851a35c | ||
|
|
4368bdb2bc | ||
|
|
1b753720c1 | ||
|
|
39e27d2a23 | ||
|
|
45e630fc60 | ||
|
|
0c35a77d4b | ||
|
|
07ea9694a3 | ||
|
|
85043e7d88 | ||
|
|
d3c1fce761 | ||
|
|
33e98bf61a | ||
|
|
cae7cb7002 | ||
|
|
b8047f0912 | ||
|
|
de98e434d6 | ||
|
|
74e5e2163a | ||
|
|
9464d4d647 | ||
|
|
b7a7e5a0d3 | ||
|
|
b9c62b3fed | ||
|
|
d10525a714 | ||
|
|
390cd756e8 | ||
|
|
ee3f734b7b | ||
|
|
905ca94f71 | ||
|
|
53d42b15b8 | ||
|
|
17cb788193 | ||
|
|
f2115b15f7 | ||
|
|
0805f362d3 | ||
|
|
eeb0113dc5 | ||
|
|
648c4caca1 | ||
|
|
2eeec22868 | ||
|
|
ef50edb9e2 | ||
|
|
3ed80fa867 | ||
|
|
cf664ca2a0 | ||
|
|
485c561b18 | ||
|
|
766698cb3a | ||
|
|
082793290a |
@@ -74,7 +74,7 @@ jobs:
|
||||
name: yarn
|
||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
|
||||
# Store yarn cache
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
|
||||
@@ -3,10 +3,10 @@ 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 "/DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
|
||||
# uncomment the dev server env variable
|
||||
sed -i -e '/DEV_SERVER_PUBLIC/s/^# //' .env
|
||||
# fix the error with dev server
|
||||
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
|
||||
# fix the error with webpacker
|
||||
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
|
||||
|
||||
# codespaces make the ports public
|
||||
|
||||
+1
-2
@@ -223,7 +223,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# DEV_SERVER_PUBLIC=
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
@@ -254,7 +254,6 @@ AZURE_APP_SECRET=
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
|
||||
+10
-2
@@ -7,10 +7,11 @@ module.exports = {
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
parser: '@babel/eslint-parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['html', 'prettier'],
|
||||
plugins: ['html', 'prettier', 'babel'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error'],
|
||||
camelcase: 'off',
|
||||
@@ -55,6 +56,13 @@ module.exports = {
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: 'config/webpack/resolve.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
jest: true,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## All javascript files should be reviewed by pranav before merging
|
||||
*.js @pranavrajs
|
||||
*.vue @pranavrajs
|
||||
|
||||
|
||||
## All enterprise related files should be reviewed by sojan before merging
|
||||
/enterprise/* @sojan-official
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for log lines and calculate percentage
|
||||
run: |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# # This action will try to install and setup
|
||||
# # chatwoot on an Ubuntu 20.04 machine using
|
||||
# # the linux installer script.
|
||||
# #
|
||||
# #
|
||||
# # This is set to run daily at midnight.
|
||||
# #
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo ./install.sh --install < input
|
||||
|
||||
# disabling http verify for now as http
|
||||
# disabling http verify for now as http
|
||||
# access to port 3000 fails in gh action env
|
||||
# - name: Verify
|
||||
# if: always()
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# curl http://localhost:3000/api
|
||||
|
||||
- name: Upload chatwoot setup log file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: chatwoot-setup-log-file
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -76,11 +76,11 @@ jobs:
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
|
||||
+1
-9
@@ -75,12 +75,4 @@ yalc.lock
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
|
||||
/storybook-static
|
||||
|
||||
# Vite Ruby
|
||||
/public/vite*
|
||||
node_modules
|
||||
# Vite uses dotenv and suggests to ignore local-only env files. See
|
||||
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||
*.local
|
||||
|
||||
/storybook-static
|
||||
+11
-70
@@ -2,7 +2,6 @@ require:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
@@ -12,7 +11,8 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -50,6 +50,7 @@ Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
- '**/routes.rb'
|
||||
@@ -102,84 +103,31 @@ RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||
- 'app/models/message.rb'
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
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'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161110102609_removeinboxid.rb'
|
||||
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20200121190901_create_account_users.rb'
|
||||
- 'db/migrate/20170211092540_notnullableusers.rb'
|
||||
- 'db/migrate/20170403095203_contactadder.rb'
|
||||
- 'db/migrate/20170406104018_add_default_status_conv.rb'
|
||||
- 'db/migrate/20170511134418_latlong.rb'
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
|
||||
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
|
||||
- 'db/migrate/20200606132552_create_labels.rb'
|
||||
- 'db/migrate/20201027135006_create_working_hours.rb'
|
||||
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
|
||||
- 'db/migrate/20210114202310_create_teams.rb'
|
||||
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
|
||||
- 'db/migrate/20210428135041_add_campaigns.rb'
|
||||
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
|
||||
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
|
||||
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
|
||||
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
|
||||
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
|
||||
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
|
||||
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
|
||||
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
|
||||
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
|
||||
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
|
||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
@@ -187,6 +135,8 @@ RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
@@ -203,13 +153,4 @@ AllCops:
|
||||
- 'config/environments/**/*'
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
||||
- db/migrate/20200503151130_add_account_feature_flag.rb
|
||||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||
- db/migrate/20220809104508_revert_cascading_indexes.rb
|
||||
- 'db/migrate/20230426130150_init_schema.rb'
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 1
|
||||
Lint/DuplicateMethods:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Lint/RescueException:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: AllowKeywordBlockArguments.
|
||||
Lint/UnderscorePrefixedVariableName:
|
||||
Exclude:
|
||||
- 'app/models/account.rb'
|
||||
- 'deploy/before_symlink.rb'
|
||||
|
||||
# Offense count: 18
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/facebook_indicators_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/listeners/reporting_listener.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
|
||||
# Offense count: 14
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
# ExcludedMethods: refine
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
|
||||
# Offense count: 2
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 10
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
|
||||
# Offense count: 1
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 8
|
||||
|
||||
# Offense count: 6
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
||||
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'app/controllers/api/base_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
- 'app/controllers/api/v1/webhooks_controller.rb'
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'lib/integrations/widget/outgoing_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxKeyValuePairs.
|
||||
Performance/RedundantMerge:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Performance/StringReplacement:
|
||||
Exclude:
|
||||
- 'lib/events/base.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: Prefixes.
|
||||
# Prefixes: when, with, without
|
||||
RSpec/ContextWording:
|
||||
Exclude:
|
||||
- 'spec/models/contact_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeClass:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeSymbol:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowConsecutiveOneLiners.
|
||||
RSpec/EmptyLineAfterExample:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Max.
|
||||
RSpec/ExampleLength:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_line_only, single_statement_only, disallow
|
||||
RSpec/ImplicitSubject:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Configuration parameters: AggregateFailuresByDefault.
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: not_to, to_not
|
||||
RSpec/NotToNot:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
Rails/ActiveRecordAliases:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/agents_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/canned_responses_controller.rb'
|
||||
- 'app/controllers/api/v1/contacts_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Rails/BelongsTo:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/EnumHash:
|
||||
Exclude:
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/HasManyOrHasOneDependent:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/InverseOf:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/controllers/**/*.rb
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
Exclude:
|
||||
- 'app/controllers/home_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
- 'app/builders/account_builder.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: strict, flexible
|
||||
Rails/TimeZone:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'lib/reports/update_account_identity.rb'
|
||||
- 'lib/reports/update_agent_identity.rb'
|
||||
- 'lib/reports/update_identity.rb'
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/Validation:
|
||||
Exclude:
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 15
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
- 'app/controllers/api/v1/inbox_members_controller.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/channel/web_widget.rb'
|
||||
- 'app/presenters/conversations/event_data_presenter.rb'
|
||||
- 'app/services/facebook/send_reply_service.rb'
|
||||
- 'lib/integrations/facebook/delivery_status.rb'
|
||||
- 'lib/integrations/facebook/message_creator.rb'
|
||||
- 'lib/integrations/facebook/message_parser.rb'
|
||||
- 'lib/integrations/widget/incoming_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/CommentedKeyword:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/assignments_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/labels_controller.rb'
|
||||
- 'app/controllers/api/v1/labels_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowIfModifier.
|
||||
Style/IfInsideElse:
|
||||
Exclude:
|
||||
- 'app/finders/conversation_finder.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/MixinUsage:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||
# SupportedStyles: predicate, comparison
|
||||
Style/NumericPredicate:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: implicit, explicit
|
||||
Style/RescueStandardError:
|
||||
Exclude:
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
@@ -2,6 +2,7 @@ import { addDecorator } from '@storybook/vue';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
@@ -13,6 +14,7 @@ import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
|
||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||
@@ -30,7 +32,7 @@ addDecorator(() => ({
|
||||
template: '<story/>',
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
beforeCreate: function () {
|
||||
beforeCreate: function() {
|
||||
this.$root._i18n = this.$i18n;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -64,7 +64,7 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'vite_rails'
|
||||
gem 'webpacker'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
|
||||
+23
-25
@@ -216,7 +216,6 @@ GEM
|
||||
railties (>= 3.2)
|
||||
down (5.4.0)
|
||||
addressable (~> 2.8)
|
||||
dry-cli (1.0.0)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
elastic-apm (4.6.2)
|
||||
@@ -278,7 +277,7 @@ GEM
|
||||
gli (2.21.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gmail_xoauth (0.4.2)
|
||||
gmail_xoauth (0.4.3)
|
||||
oauth (>= 0.3.6)
|
||||
google-apis-core (0.11.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
@@ -317,16 +316,16 @@ GEM
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.22.3)
|
||||
google-protobuf (3.22.3-arm64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-linux)
|
||||
google-protobuf (3.25.2)
|
||||
google-protobuf (3.25.2-arm64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.6.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (1.11.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
@@ -336,13 +335,13 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.54.0)
|
||||
grpc (1.54.3)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-darwin)
|
||||
grpc (1.54.3-x86_64-darwin)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-linux)
|
||||
grpc (1.54.3-x86_64-linux)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
@@ -489,14 +488,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.0)
|
||||
nokogiri (1.16.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-arm64-darwin)
|
||||
nokogiri (1.16.2-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-darwin)
|
||||
nokogiri (1.16.2-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
nokogiri (1.16.2-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oauth (1.1.0)
|
||||
@@ -571,7 +570,7 @@ GEM
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
@@ -709,6 +708,7 @@ GEM
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.14.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.14.0)
|
||||
@@ -794,14 +794,7 @@ GEM
|
||||
valid_email2 (4.0.6)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.2)
|
||||
vite_rails (3.0.17)
|
||||
railties (>= 5.1, < 8)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
vite_ruby (3.5.0)
|
||||
dry-cli (>= 0.7, < 2)
|
||||
rack-proxy (~> 0.6, >= 0.6.1)
|
||||
zeitwerk (~> 2.2)
|
||||
version_gem (1.1.3)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@@ -817,6 +810,11 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.4.4)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
@@ -959,10 +957,10 @@ DEPENDENCIES
|
||||
tzinfo-data
|
||||
uglifier
|
||||
valid_email2
|
||||
vite_rails
|
||||
web-console (>= 4.2.1)
|
||||
web-push
|
||||
webmock
|
||||
webpacker
|
||||
wisper (= 2.0.0)
|
||||
working_hours
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||
Copyright (c) 2017-2024 Chatwoot Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
backend: bin/rails s -p 3000
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
frontend: bin/vite dev
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/vite
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
padding: 4px 12px;
|
||||
|
||||
.icon-container {
|
||||
margin-right: 4px;
|
||||
margin-right: 2px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -86,5 +86,8 @@ $swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
||||
pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
@@ -15,12 +15,22 @@ class AccountBuilder
|
||||
end
|
||||
[@user, @account]
|
||||
rescue StandardError => e
|
||||
puts e.inspect
|
||||
Rails.logger.debug e.inspect
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_full_name
|
||||
# the empty string ensures that not-null constraint is not violated
|
||||
@user_full_name || ''
|
||||
end
|
||||
|
||||
def account_name
|
||||
# the empty string ensures that not-null constraint is not violated
|
||||
@account_name || ''
|
||||
end
|
||||
|
||||
def validate_email
|
||||
address = ValidEmail2::Address.new(@email)
|
||||
if address.valid? # && !address.disposable?
|
||||
@@ -39,7 +49,7 @@ class AccountBuilder
|
||||
end
|
||||
|
||||
def create_account
|
||||
@account = Account.create!(name: @account_name, locale: I18n.locale)
|
||||
@account = Account.create!(name: account_name, locale: I18n.locale)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
@@ -64,7 +74,7 @@ class AccountBuilder
|
||||
@user = User.new(email: @email,
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
name: user_full_name)
|
||||
@user.type = 'SuperAdmin' if @super_admin
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# The AgentBuilder class is responsible for creating a new agent.
|
||||
# It initializes with necessary attributes and provides a perform method
|
||||
# to create a user and account user in a transaction.
|
||||
class AgentBuilder
|
||||
# Initializes an AgentBuilder with necessary attributes.
|
||||
# @param email [String] the email of the user.
|
||||
# @param name [String] the name of the user.
|
||||
# @param role [String] the role of the user, defaults to 'agent' if not provided.
|
||||
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
|
||||
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
|
||||
# @param auto_offline [Boolean] the auto offline status of the user.
|
||||
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
|
||||
|
||||
# Creates a user and account user in a transaction.
|
||||
# @return [User] the created user.
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@user = find_or_create_user
|
||||
send_confirmation_if_required
|
||||
create_account_user
|
||||
end
|
||||
@user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Finds a user by email or creates a new one with a temporary password.
|
||||
# @return [User] the found or created user.
|
||||
def find_or_create_user
|
||||
user = User.find_by(email: email)
|
||||
return user if user
|
||||
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
|
||||
end
|
||||
|
||||
# Sends confirmation instructions if the user is persisted and not confirmed.
|
||||
def send_confirmation_if_required
|
||||
@user.send_confirmation_instructions if user_needs_confirmation?
|
||||
end
|
||||
|
||||
# Checks if the user needs confirmation.
|
||||
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
|
||||
def user_needs_confirmation?
|
||||
@user.persisted? && !@user.confirmed?
|
||||
end
|
||||
|
||||
# Creates an account user linking the user to the current account.
|
||||
def create_account_user
|
||||
AccountUser.create!({
|
||||
account_id: account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: inviter.id
|
||||
}.merge({
|
||||
role: role,
|
||||
availability: availability,
|
||||
auto_offline: auto_offline
|
||||
}.compact))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:user_id
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class V2::Reports::BaseSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
private
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
'conversations.team_id'
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.each_with_object([]) do |team, arr|
|
||||
arr << {
|
||||
id: team.id,
|
||||
conversations_count: @grouped_conversations_count[team.id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,26 @@
|
||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
||||
before_action :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :create_user, only: [:create]
|
||||
before_action :save_account_user, only: [:create]
|
||||
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
|
||||
|
||||
def index
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
builder = AgentBuilder.new(
|
||||
email: new_agent_params['email'],
|
||||
name: new_agent_params['name'],
|
||||
role: new_agent_params['role'],
|
||||
availability: new_agent_params['availability'],
|
||||
auto_offline: new_agent_params['auto_offline'],
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
|
||||
builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@@ -23,6 +33,30 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def bulk_create
|
||||
emails = params[:emails]
|
||||
|
||||
emails.each do |email|
|
||||
builder = AgentBuilder.new(
|
||||
email: email,
|
||||
name: email.split('@').first,
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
begin
|
||||
builder.perform
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
|
||||
# This endpoint is used to bulk create agents during onboarding
|
||||
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -33,47 +67,34 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.find_by(email: new_agent_params[:email])
|
||||
end
|
||||
|
||||
# TODO: move this to a builder and combine the save account user method into a builder
|
||||
# ensure the account user association is also created in a single transaction
|
||||
def create_user
|
||||
return @user.send_confirmation_instructions if @user
|
||||
|
||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: current_user.id
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def validate_limit_for_bulk_create
|
||||
limit_available = params[:emails].count <= available_agent_count
|
||||
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent?
|
||||
end
|
||||
|
||||
def available_agent_count
|
||||
Current.account.usage_limits[:agents] - agents.count
|
||||
end
|
||||
|
||||
def can_add_agent?
|
||||
available_agent_count.positive?
|
||||
end
|
||||
|
||||
def delete_user_record(agent)
|
||||
|
||||
@@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def export
|
||||
column_names = params['column_names']
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email)
|
||||
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||
end
|
||||
|
||||
@@ -148,7 +148,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
|
||||
@@ -2,13 +2,13 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
RESULTS_PER_PAGE = 15
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :fetch_notification, only: [:update, :destroy, :snooze]
|
||||
before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@notifications = notification_finder.notifications
|
||||
@unread_count = notification_finder.unread_count
|
||||
@notifications = notification_finder.perform
|
||||
@count = notification_finder.count
|
||||
end
|
||||
|
||||
@@ -29,18 +29,33 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def unread
|
||||
@notification.update(read_at: nil)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
if params[:type] == 'read'
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
|
||||
else
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@unread_count = notification_finder.unread_count
|
||||
render json: @unread_count
|
||||
end
|
||||
|
||||
def snooze
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
|
||||
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :ensure_account_name, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::InvalidParams,
|
||||
CustomExceptions::Account::UserExists,
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
@@ -38,11 +40,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
def cache_keys
|
||||
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
render json: { cache_keys: cache_keys_for_account }, status: :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
@@ -53,7 +58,18 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def get_cache_keys
|
||||
def ensure_account_name
|
||||
# ensure that account_name and user_full_name is present
|
||||
# this is becuase the account builder and the models validations are not triggered
|
||||
# this change is to align the behaviour with the v2 accounts controller
|
||||
# since these values are not required directly there
|
||||
return if account_params[:account_name].present?
|
||||
return if account_params[:user_full_name].present?
|
||||
|
||||
raise CustomExceptions::Account::InvalidParams.new({})
|
||||
end
|
||||
|
||||
def cache_keys_for_account
|
||||
{
|
||||
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
||||
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
||||
@@ -70,6 +86,10 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.permit(:industry, :company_size, :timezone)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
@@ -10,7 +10,9 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(password_params.except(:current_password))
|
||||
end
|
||||
|
||||
@user.update!(profile_params)
|
||||
@user.assign_attributes(profile_params)
|
||||
@user.custom_attributes.merge!(custom_attributes_params)
|
||||
@user.save!
|
||||
end
|
||||
|
||||
def avatar
|
||||
@@ -31,6 +33,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def resend_confirmation
|
||||
@user.send_confirmation_instructions unless @user.confirmed?
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@@ -57,6 +64,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.require(:profile).permit(:phone_number)
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:profile).permit(
|
||||
:current_password,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
end
|
||||
|
||||
def team
|
||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize :report, :view?
|
||||
end
|
||||
|
||||
def prepare_builder_params
|
||||
@builder_params = {
|
||||
since: permitted_params[:since],
|
||||
until: permitted_params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:since, :until, :business_hours)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
class Api::V2::AccountsController < Api::BaseController
|
||||
include AuthHelper
|
||||
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::UserExists,
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
|
||||
def create
|
||||
@user, @account = AccountBuilder.new(
|
||||
email: account_params[:email],
|
||||
user_password: account_params[:password],
|
||||
locale: account_params[:locale],
|
||||
user: current_user
|
||||
).perform
|
||||
|
||||
fetch_account_and_user_info
|
||||
update_account_info if @account.present?
|
||||
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account_attributes
|
||||
{
|
||||
custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' })
|
||||
}
|
||||
end
|
||||
|
||||
def update_account_info
|
||||
@account.update!(
|
||||
account_attributes
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_account_and_user_info; end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
def validate_captcha
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::AccountsController.prepend_mod_with('Api::V2::AccountsController')
|
||||
@@ -1,7 +1,8 @@
|
||||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => %w[toggle_status create],
|
||||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create],
|
||||
'api/v1/accounts/conversations/messages' => ['create'],
|
||||
'api/v1/accounts/conversations/assignments' => ['create']
|
||||
}.freeze
|
||||
|
||||
def ensure_access_token
|
||||
|
||||
@@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: 'v14.0',
|
||||
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
|
||||
GIT_SHA: GIT_HASH
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||
include Events::Types
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
|
||||
|
||||
def index
|
||||
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@conversation = create_conversation
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
# Check if the conversation is already resolved to prevent redundant operations
|
||||
return if @conversation.resolved?
|
||||
|
||||
# Assign the conversation's contact as the resolver
|
||||
# This step attributes the resolution action to the contact involved in the conversation
|
||||
# If this assignment is not made, the system implicitly becomes the resolver by default
|
||||
Current.contact = @conversation.contact
|
||||
|
||||
# Update the conversation's status to 'resolved' to reflect its closure
|
||||
@conversation.status = :resolved
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
@@ -30,7 +46,11 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
@conversation = if @contact_inbox.hmac_verified?
|
||||
@contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
else
|
||||
@contact_inbox.conversations.find_by!(display_id: params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
|
||||
@@ -22,19 +22,24 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
i.value = value
|
||||
i.save!
|
||||
end
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_config
|
||||
@config = params[:config]
|
||||
@config = params[:config] || 'general'
|
||||
end
|
||||
|
||||
def allowed_configs
|
||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
||||
@allowed_configs = case @config
|
||||
when 'facebook'
|
||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||
when 'email'
|
||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||
else
|
||||
%w[ENABLE_ACCOUNT_SIGNUP]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
end
|
||||
|
||||
def sha
|
||||
sha = `git rev-parse HEAD`
|
||||
@metrics['Git SHA'] = sha.presence || 'n/a'
|
||||
@metrics['Git SHA'] = GIT_HASH
|
||||
end
|
||||
|
||||
def postgres_status
|
||||
|
||||
@@ -169,6 +169,12 @@ class ConversationFinder
|
||||
)
|
||||
|
||||
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
@conversations = @conversations.send(sort_by, sort_order)
|
||||
|
||||
if params[:updated_within].present?
|
||||
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
|
||||
else
|
||||
@conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,8 @@ class NotificationFinder
|
||||
set_up
|
||||
end
|
||||
|
||||
def perform
|
||||
notifications
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@@ -26,22 +26,31 @@ class NotificationFinder
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_by_status
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||
def filter_snoozed_notifications
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def fitler_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
def type_included?(type)
|
||||
(params[:includes] || []).include?(type)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
|
||||
def sort_order
|
||||
params[:sort_order] || :desc
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
dates = data.pluck(:date).uniq.sort
|
||||
|
||||
# add the dates as the first row, leave an empty cell for the hour column
|
||||
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << ([nil] + dates)
|
||||
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << (['Start of the hour'] + dates)
|
||||
|
||||
# group the data by hour, we do not need to sort it, because the data is already sorted
|
||||
# given it starts from the beginning of the day
|
||||
@@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
||||
data.group_by { |d| d[:hour] }.each do |hour, items|
|
||||
# create a new row for each hour
|
||||
row = [hour]
|
||||
row = [format('%02d:00', hour)]
|
||||
|
||||
# group the items by date, so we can easily access the value for each date
|
||||
# grouped values will be a hasg with the date as the key, and the value as the value
|
||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
||||
end
|
||||
|
||||
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
|
||||
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4]
|
||||
# add the row to the result array
|
||||
|
||||
result_arr << row
|
||||
@@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# return the resultant array
|
||||
# the result looks like this
|
||||
# [
|
||||
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# [0, 0, 0, 0],
|
||||
# [1, 0, 0, 0],
|
||||
# [2, 0, 0, 0],
|
||||
# [3, 0, 0, 0],
|
||||
# [4, 0, 0, 0],
|
||||
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# ['00:00', 0, 0, 0],
|
||||
# ['01:00', 0, 0, 0],
|
||||
# ['02:00', 0, 0, 0],
|
||||
# ['03:00', 0, 0, 0],
|
||||
# ['04:00', 0, 0, 0],
|
||||
# ]
|
||||
result_arr
|
||||
end
|
||||
|
||||
@@ -45,12 +45,8 @@ module Api::V2::Accounts::ReportsHelper
|
||||
def generate_readable_report_metrics(report_metric)
|
||||
[
|
||||
report_metric[:conversations_count],
|
||||
time_to_minutes(report_metric[:avg_first_response_time]),
|
||||
time_to_minutes(report_metric[:avg_resolution_time])
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format
|
||||
]
|
||||
end
|
||||
|
||||
def time_to_minutes(time_in_seconds)
|
||||
(time_in_seconds / 60).to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module SuperAdmin::AccountFeaturesHelper
|
||||
def self.account_features
|
||||
YAML.safe_load(Rails.root.join('config/features.yml').read).freeze
|
||||
end
|
||||
|
||||
def self.account_premium_features
|
||||
account_features.filter { |feature| feature['premium'] }.pluck('name')
|
||||
end
|
||||
end
|
||||
@@ -2,13 +2,14 @@
|
||||
<div
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="currentAccountId">
|
||||
<payment-pending-banner />
|
||||
<pending-email-verification-banner v-if="hideOnOnboardingView" />
|
||||
<payment-pending-banner v-if="hideOnOnboardingView" />
|
||||
<upgrade-banner />
|
||||
</template>
|
||||
<transition name="fade" mode="out-in">
|
||||
@@ -32,10 +33,12 @@ import NetworkNotification from './components/NetworkNotification.vue';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -52,6 +55,7 @@ export default {
|
||||
PaymentPendingBanner,
|
||||
WootSnackbarBox,
|
||||
UpgradeBanner,
|
||||
PendingEmailVerificationBanner,
|
||||
},
|
||||
|
||||
mixins: [rtlMixin],
|
||||
@@ -76,6 +80,9 @@ export default {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
},
|
||||
hideOnOnboardingView() {
|
||||
return !isOnOnboardingView(this.$route);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
|
||||
bulkInvite({ emails }) {
|
||||
return axios.post(`${this.url}/bulk_create`, {
|
||||
emails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Agents();
|
||||
|
||||
@@ -29,11 +29,12 @@ export default {
|
||||
return fetchPromise;
|
||||
},
|
||||
hasAuthCookie() {
|
||||
return !!Cookies.getJSON('cw_d_session_info');
|
||||
return !!Cookies.get('cw_d_session_info');
|
||||
},
|
||||
getAuthData() {
|
||||
if (this.hasAuthCookie()) {
|
||||
return Cookies.getJSON('cw_d_session_info');
|
||||
const savedAuthInfo = Cookies.get('cw_d_session_info');
|
||||
return JSON.parse(savedAuthInfo || '{}');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -97,4 +98,8 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
resendConfirmation() {
|
||||
const urlData = endPoints('resendConfirmation');
|
||||
return axios.post(urlData.url);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,6 +47,10 @@ const endPoints = {
|
||||
setActiveAccount: {
|
||||
url: '/api/v1/profile/set_active_account',
|
||||
},
|
||||
|
||||
resendConfirmation: {
|
||||
url: '/api/v1/profile/resend_confirmation',
|
||||
},
|
||||
};
|
||||
|
||||
export default page => {
|
||||
|
||||
@@ -6,8 +6,16 @@ class NotificationsAPI extends ApiClient {
|
||||
super('notifications', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page) {
|
||||
return axios.get(`${this.url}?page=${page}`);
|
||||
get({ page, status, type, sortOrder }) {
|
||||
const includesFilter = [status, type].filter(value => !!value);
|
||||
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
sort_order: sortOrder,
|
||||
includes: includesFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(contactId) {
|
||||
@@ -25,9 +33,29 @@ class NotificationsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
unRead(id) {
|
||||
return axios.post(`${this.url}/${id}/unread`);
|
||||
}
|
||||
|
||||
readAll() {
|
||||
return axios.post(`${this.url}/read_all`);
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
return axios.delete(`${this.url}/${id}`);
|
||||
}
|
||||
|
||||
deleteAll({ type = 'all' }) {
|
||||
return axios.post(`${this.url}/destroy_all`, {
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
snooze({ id, snoozedUntil = null }) {
|
||||
return axios.post(`${this.url}/${id}/snooze`, {
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationsAPI();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class SlaAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('sla_policies', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new SlaAPI();
|
||||
@@ -10,4 +10,29 @@ describe('#AgentAPI', () => {
|
||||
expect(agents).toHaveProperty('update');
|
||||
expect(agents).toHaveProperty('delete');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#bulkInvite', () => {
|
||||
agents.bulkInvite({ emails: ['hello@hi.com'] });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/agents/bulk_create',
|
||||
{
|
||||
emails: ['hello@hi.com'],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,11 +27,37 @@ describe('#NotificationAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get(1);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications?page=1'
|
||||
);
|
||||
describe('#get', () => {
|
||||
it('generates the API call if both params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'snoozed',
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['snoozed', 'read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the API call if one of the params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('#getNotifications', () => {
|
||||
@@ -65,5 +91,30 @@ describe('#NotificationAPI', () => {
|
||||
'/api/v1/notifications/read_all'
|
||||
);
|
||||
});
|
||||
|
||||
it('#snooze', () => {
|
||||
notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/1/snooze',
|
||||
{
|
||||
snoozed_until: 12332211,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
notificationsAPI.delete(1);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1');
|
||||
});
|
||||
|
||||
it('#deleteAll', () => {
|
||||
notificationsAPI.deleteAll({ type: 'all' });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/destroy_all',
|
||||
{
|
||||
type: 'all',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'vue2-datepicker/scss/index';
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.date-picker {
|
||||
&.no-margin {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.formulate-input {
|
||||
.formulate-input-errors {
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
// Foundation for Sites Settings
|
||||
// -----------------------------
|
||||
//
|
||||
// Table of Contents:
|
||||
//
|
||||
// 1. Global
|
||||
// 2. Breakpoints
|
||||
// 3. The Grid
|
||||
// 4. Base Typography
|
||||
// 5. Typography Helpers
|
||||
// 6. Abide
|
||||
// 7. Accordion
|
||||
// 8. Accordion Menu
|
||||
// 9. Badge
|
||||
// 10. Breadcrumbs
|
||||
// 11. Button
|
||||
// 12. Button Group
|
||||
// 13. Callout
|
||||
// 14. Card
|
||||
// 15. Close Button
|
||||
// 16. Drilldown
|
||||
// 17. Dropdown
|
||||
// 18. Dropdown Menu
|
||||
// 19. Forms
|
||||
// 20. Label
|
||||
// 21. Media Object
|
||||
// 22. Menu
|
||||
// 23. Meter
|
||||
// 24. Off-canvas
|
||||
// 25. Orbit
|
||||
// 26. Pagination
|
||||
// 27. Progress Bar
|
||||
// 28. Responsive Embed
|
||||
// 29. Reveal
|
||||
// 30. Slider
|
||||
// 31. Switch
|
||||
// 32. Table
|
||||
// 33. Tabs
|
||||
// 34. Thumbnail
|
||||
// 35. Title Bar
|
||||
// 36. Tooltip
|
||||
// 37. Top Bar
|
||||
|
||||
@import '~foundation-sites/scss/util/util';
|
||||
// 1. Global
|
||||
// ---------
|
||||
|
||||
// Disable contrast warnings in Foundation.
|
||||
$contrast-warnings: false;
|
||||
|
||||
$global-font-size: 16px;
|
||||
$global-width: 100%;
|
||||
$global-lineheight: 1.5;
|
||||
$foundation-palette: (primary: $color-woot,
|
||||
secondary: #5d7592,
|
||||
success: #44ce4b,
|
||||
warning: #ffc532,
|
||||
alert: #ff382d);
|
||||
$light-gray: #c0ccda;
|
||||
$medium-gray: #8492a6;
|
||||
$dark-gray: $color-gray;
|
||||
$black: #000;
|
||||
$white: #fff;
|
||||
$body-background: $white;
|
||||
$body-font-color: $color-body;
|
||||
$body-font-family: 'PlusJakarta',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif;
|
||||
$body-antialiased: true;
|
||||
$global-margin: $space-small;
|
||||
$global-padding: $space-small;
|
||||
$global-weight-normal: normal;
|
||||
$global-weight-bold: bold;
|
||||
$global-radius: 0;
|
||||
$global-text-direction: ltr;
|
||||
$global-flexbox: false;
|
||||
$print-transparent-backgrounds: true;
|
||||
|
||||
@include add-foundation-colors;
|
||||
|
||||
// 2. Breakpoints
|
||||
// --------------
|
||||
|
||||
$breakpoints: (small: 0,
|
||||
medium: 640px,
|
||||
large: 1024px,
|
||||
xlarge: 1200px,
|
||||
xxlarge: 1400px,
|
||||
xxxlarge: 1600px,
|
||||
);
|
||||
$print-breakpoint: large;
|
||||
$breakpoint-classes: (small medium large);
|
||||
|
||||
// 3. The Grid
|
||||
// -----------
|
||||
|
||||
$grid-row-width: $global-width;
|
||||
$grid-column-count: 12;
|
||||
$grid-column-gutter: (small: $zero,
|
||||
medium: $zero);
|
||||
$grid-column-align-edge: true;
|
||||
$block-grid-max: 8;
|
||||
|
||||
// 4. Base Typography
|
||||
// ------------------
|
||||
|
||||
$header-font-family: $body-font-family;
|
||||
$header-font-weight: $font-weight-medium;
|
||||
$header-font-style: normal;
|
||||
$font-family-monospace: $body-font-family;
|
||||
$header-color: $color-heading;
|
||||
$header-lineheight: 1.4;
|
||||
$header-margin-bottom: 0.3125rem;
|
||||
$header-styles: (small: ("h1": ("font-size": 24),
|
||||
"h2": ("font-size": 20),
|
||||
"h3": ("font-size": 19),
|
||||
"h4": ("font-size": 18),
|
||||
"h5": ("font-size": 17),
|
||||
"h6": ("font-size": 16)),
|
||||
medium: ("h1": ("font-size": 48),
|
||||
"h2": ("font-size": 40),
|
||||
"h3": ("font-size": 31),
|
||||
"h4": ("font-size": 25),
|
||||
"h5": ("font-size": 20),
|
||||
"h6": ("font-size": 16)));
|
||||
$header-text-rendering: optimizeLegibility;
|
||||
$small-font-size: 80%;
|
||||
$header-small-font-color: $medium-gray;
|
||||
$paragraph-lineheight: 1.65;
|
||||
$paragraph-margin-bottom: var(--space-small);
|
||||
$paragraph-text-rendering: optimizeLegibility;
|
||||
$code-color: $black;
|
||||
$code-font-family: $font-family-monospace;
|
||||
$code-font-weight: $global-weight-normal;
|
||||
$code-background: $light-gray;
|
||||
$code-border: 1px solid $medium-gray;
|
||||
$code-padding: rem-calc(2 5 1);
|
||||
$anchor-color: $primary-color;
|
||||
$anchor-color-hover: scale-color($anchor-color, $lightness: -14%);
|
||||
$anchor-text-decoration: none;
|
||||
$anchor-text-decoration-hover: none;
|
||||
$hr-width: $global-width;
|
||||
$hr-border: 1px solid $medium-gray;
|
||||
$hr-margin: rem-calc(20) auto;
|
||||
$list-lineheight: $paragraph-lineheight;
|
||||
$list-margin-bottom: $paragraph-margin-bottom;
|
||||
$list-style-type: disc;
|
||||
$list-style-position: outside;
|
||||
$list-side-margin: 0.78125rem;
|
||||
$list-nested-side-margin: 0.78125rem;
|
||||
$defnlist-margin-bottom: 0.6875rem;
|
||||
$defnlist-term-weight: $global-weight-bold;
|
||||
$defnlist-term-margin-bottom: 0.1875rem;
|
||||
$blockquote-color: $dark-gray;
|
||||
$blockquote-padding: rem-calc(9 20 0 19);
|
||||
$blockquote-border: 1px solid $medium-gray;
|
||||
$cite-font-size: rem-calc(13);
|
||||
$cite-color: $dark-gray;
|
||||
$cite-pseudo-content: '\2014 \0020';
|
||||
$keystroke-font: $font-family-monospace;
|
||||
$keystroke-color: $black;
|
||||
$keystroke-background: $light-gray;
|
||||
$keystroke-padding: rem-calc(2 4 0);
|
||||
$keystroke-radius: $global-radius;
|
||||
$abbr-underline: 1px dotted $black;
|
||||
|
||||
// 5. Typography Helpers
|
||||
// ---------------------
|
||||
|
||||
$lead-font-size: $global-font-size * 1.25;
|
||||
$lead-lineheight: 1.6;
|
||||
$subheader-lineheight: 1.4;
|
||||
$subheader-color: $dark-gray;
|
||||
$subheader-font-weight: $global-weight-normal;
|
||||
$subheader-margin-top: 0.125rem;
|
||||
$subheader-margin-bottom: 0.3125rem;
|
||||
$stat-font-size: 1.5625rem;
|
||||
|
||||
// 6. Abide
|
||||
// --------
|
||||
|
||||
$abide-inputs: true;
|
||||
$abide-labels: true;
|
||||
$input-background-invalid: get-color(alert);
|
||||
$form-label-color-invalid: get-color(alert);
|
||||
$input-error-color: get-color(alert);
|
||||
$input-error-font-size: rem-calc(12);
|
||||
$input-error-font-weight: $global-weight-bold;
|
||||
|
||||
// 7. Accordion
|
||||
// ------------
|
||||
|
||||
$accordion-background: $white;
|
||||
$accordion-plusminus: true;
|
||||
$accordion-title-font-size: rem-calc(12);
|
||||
$accordion-item-color: $primary-color;
|
||||
$accordion-item-background-hover: $light-gray;
|
||||
$accordion-item-padding: 0.78125rem 0.625rem;
|
||||
$accordion-content-background: $white;
|
||||
$accordion-content-border: 1px solid $light-gray;
|
||||
$accordion-content-color: $body-font-color;
|
||||
$accordion-content-padding: 0.625rem;
|
||||
|
||||
// 8. Accordion Menu
|
||||
// -----------------
|
||||
|
||||
$accordionmenu-arrows: true;
|
||||
$accordionmenu-arrow-color: $primary-color;
|
||||
$accordionmenu-arrow-size: 6px;
|
||||
|
||||
// 9. Badge
|
||||
// --------
|
||||
|
||||
$badge-background: $primary-color;
|
||||
$badge-color: $white;
|
||||
$badge-color-alt: $black;
|
||||
$badge-palette: $foundation-palette;
|
||||
$badge-padding: var(--space-smaller);
|
||||
$badge-minwidth: 2.1em;
|
||||
$badge-font-size: var(--font-size-nano);
|
||||
|
||||
// 10. Breadcrumbs
|
||||
// ---------------
|
||||
|
||||
$breadcrumbs-margin: 0 0 $global-margin 0;
|
||||
$breadcrumbs-item-font-size: rem-calc(11);
|
||||
$breadcrumbs-item-color: $primary-color;
|
||||
$breadcrumbs-item-color-current: $black;
|
||||
$breadcrumbs-item-color-disabled: $medium-gray;
|
||||
$breadcrumbs-item-margin: 0.46875rem;
|
||||
$breadcrumbs-item-uppercase: true;
|
||||
$breadcrumbs-item-slash: true;
|
||||
|
||||
// 11. Button
|
||||
// ----------
|
||||
|
||||
$button-padding: var(--space-smaller) 1em;
|
||||
$button-margin: 0 0 $global-margin 0;
|
||||
$button-fill: solid;
|
||||
$button-background: $primary-color;
|
||||
$button-background-hover: scale-color($button-background, $lightness: -15%);
|
||||
$button-color: $white;
|
||||
$button-color-alt: $white;
|
||||
$button-radius: var(--border-radius-normal);
|
||||
$button-sizes: (tiny: var(--font-size-micro),
|
||||
small: var(--font-size-mini),
|
||||
default: var(--font-size-small),
|
||||
large: var(--font-size-medium));
|
||||
$button-palette: $foundation-palette;
|
||||
$button-opacity-disabled: 0.4;
|
||||
$button-background-hover-lightness: -20%;
|
||||
$button-hollow-hover-lightness: -50%;
|
||||
$button-transition: background-color 0.25s ease-out,
|
||||
color 0.25s ease-out;
|
||||
|
||||
// 12. Button Group
|
||||
// ----------------
|
||||
|
||||
$buttongroup-margin: 0;
|
||||
$buttongroup-spacing: 0;
|
||||
$buttongroup-child-selector: '.button';
|
||||
$buttongroup-expand-max: 6;
|
||||
$buttongroup-radius-on-each: false;
|
||||
|
||||
// 13. Callout
|
||||
// -----------
|
||||
|
||||
$callout-background: $white;
|
||||
$callout-background-fade: 85%;
|
||||
$callout-border: 1px solid rgba($black, 0.25);
|
||||
$callout-margin: 0 0 0.625rem 0;
|
||||
$callout-padding: 0.625rem;
|
||||
$callout-font-color: $body-font-color;
|
||||
$callout-font-color-alt: $body-background;
|
||||
$callout-radius: $global-radius;
|
||||
$callout-link-tint: 30%;
|
||||
|
||||
// 14. Card
|
||||
// --------
|
||||
|
||||
$card-background: $white;
|
||||
$card-font-color: $body-font-color;
|
||||
$card-divider-background: $light-gray;
|
||||
$card-border: 1px solid var(--color-border);
|
||||
$card-shadow: var(--shadow-small);
|
||||
$card-border-radius: var(--border-radius-normal);
|
||||
$card-padding: var(--space-small);
|
||||
$card-margin: $global-margin;
|
||||
|
||||
// 15. Close Button
|
||||
// ----------------
|
||||
|
||||
$closebutton-position: right top;
|
||||
$closebutton-offset-horizontal: (small: 0.66rem,
|
||||
medium: 1rem);
|
||||
$closebutton-offset-vertical: (small: 0.33em,
|
||||
medium: 0.5rem);
|
||||
$closebutton-size: (small: 1.5em,
|
||||
medium: 2em);
|
||||
$closebutton-lineheight: 1;
|
||||
$closebutton-color: $dark-gray;
|
||||
$closebutton-color-hover: $black;
|
||||
|
||||
// 16. Drilldown
|
||||
// -------------
|
||||
|
||||
$drilldown-transition: transform 0.15s linear;
|
||||
$drilldown-arrows: true;
|
||||
$drilldown-arrow-color: $primary-color;
|
||||
$drilldown-arrow-size: 6px;
|
||||
$drilldown-background: $white;
|
||||
|
||||
// 17. Dropdown
|
||||
// ------------
|
||||
|
||||
$dropdown-padding: 0.625rem;
|
||||
$dropdown-background: $body-background;
|
||||
$dropdown-border: 1px solid $medium-gray;
|
||||
$dropdown-font-size: 0.625rem;
|
||||
$dropdown-width: 300px;
|
||||
$dropdown-radius: $global-radius;
|
||||
$dropdown-sizes: (tiny: 100px,
|
||||
small: 200px,
|
||||
large: 400px);
|
||||
|
||||
// 18. Dropdown Menu
|
||||
// -----------------
|
||||
|
||||
$dropdownmenu-arrows: true;
|
||||
$dropdownmenu-arrow-color: $anchor-color;
|
||||
$dropdownmenu-arrow-size: 6px;
|
||||
$dropdownmenu-min-width: 200px;
|
||||
$dropdownmenu-background: $white;
|
||||
$dropdownmenu-border: 1px solid $medium-gray;
|
||||
|
||||
// 19. Forms
|
||||
// ---------
|
||||
|
||||
$fieldset-border: 1px solid $light-gray;
|
||||
$fieldset-padding: $space-two;
|
||||
$fieldset-margin: $space-one $zero;
|
||||
$legend-padding: rem-calc(0 3);
|
||||
$form-spacing: $space-normal;
|
||||
$helptext-color: $color-body;
|
||||
$helptext-font-size: $font-size-small;
|
||||
$helptext-font-style: italic;
|
||||
$input-prefix-color: $color-body;
|
||||
$input-prefix-background: var(--b-100);
|
||||
$input-prefix-border: 1px solid $color-border;
|
||||
$input-prefix-padding: 0.625rem;
|
||||
$form-label-color: $color-body;
|
||||
$form-label-font-size: rem-calc(14);
|
||||
$form-label-font-weight: $font-weight-medium;
|
||||
$form-label-line-height: 1.8;
|
||||
$select-background: $white;
|
||||
$select-triangle-color: $dark-gray;
|
||||
$select-radius: var(--border-radius-normal);
|
||||
$input-color: $color-body;
|
||||
$input-placeholder-color: $light-gray;
|
||||
$input-font-family: inherit;
|
||||
$input-font-size: $font-size-default;
|
||||
$input-font-weight: $global-weight-normal;
|
||||
$input-background: $white;
|
||||
$input-background-focus: $white;
|
||||
$input-background-disabled: $light-gray;
|
||||
$input-border: 1px solid var(--s-200);
|
||||
$input-border-focus: 1px solid lighten($primary-color, 15%);
|
||||
$input-shadow: 0;
|
||||
$input-shadow-focus: 0;
|
||||
$input-cursor-disabled: not-allowed;
|
||||
$input-transition: border-color 0.25s ease-in-out;
|
||||
$input-number-spinners: true;
|
||||
$input-radius: var(--border-radius-normal);
|
||||
$form-button-radius: var(--border-radius-normal);
|
||||
|
||||
// 20. Label
|
||||
// ---------
|
||||
|
||||
$label-background: $white;
|
||||
$label-color: $black;
|
||||
$label-color-alt: $black;
|
||||
$label-palette: $foundation-palette;
|
||||
$label-font-size: $font-size-mini;
|
||||
$label-padding: $space-smaller $space-small;
|
||||
$label-radius: var(--border-radius-small);
|
||||
|
||||
// 21. Media Object
|
||||
// ----------------
|
||||
|
||||
$mediaobject-margin-bottom: $global-margin;
|
||||
$mediaobject-section-padding: $global-padding;
|
||||
$mediaobject-image-width-stacked: 100%;
|
||||
|
||||
// 22. Menu
|
||||
// --------
|
||||
|
||||
$menu-margin: 0;
|
||||
$menu-margin-nested: $space-medium;
|
||||
$menu-item-padding: $space-slab;
|
||||
$menu-item-color-active: $white;
|
||||
$menu-item-background-active: $color-background;
|
||||
$menu-icon-spacing: 0.15625rem;
|
||||
$menu-item-background-hover: $light-gray;
|
||||
$menu-border: $light-gray;
|
||||
|
||||
// 23. Meter
|
||||
// ---------
|
||||
|
||||
$meter-height: 0.625rem;
|
||||
$meter-radius: $global-radius;
|
||||
$meter-background: $medium-gray;
|
||||
$meter-fill-good: $success-color;
|
||||
$meter-fill-medium: $warning-color;
|
||||
$meter-fill-bad: $alert-color;
|
||||
|
||||
// 24. Off-canvas
|
||||
// --------------
|
||||
|
||||
$offcanvas-sizes: (small: 14.375,
|
||||
medium: 14.375,
|
||||
);
|
||||
$offcanvas-vertical-sizes: (small: 14.375,
|
||||
medium: 14.375,
|
||||
);
|
||||
$offcanvas-background: $light-gray;
|
||||
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
|
||||
$offcanvas-push-zindex: 1;
|
||||
$offcanvas-overlap-zindex: 10;
|
||||
$offcanvas-reveal-zindex: 1;
|
||||
$offcanvas-transition-length: 0.5s;
|
||||
$offcanvas-transition-timing: ease;
|
||||
$offcanvas-fixed-reveal: true;
|
||||
$offcanvas-exit-background: rgba($white, 0.25);
|
||||
$maincontent-class: 'off-canvas-content';
|
||||
|
||||
// 25. Orbit
|
||||
// ---------
|
||||
|
||||
$orbit-bullet-background: $medium-gray;
|
||||
$orbit-bullet-background-active: $dark-gray;
|
||||
$orbit-bullet-diameter: 0.75rem;
|
||||
$orbit-bullet-margin: 0.0625rem;
|
||||
$orbit-bullet-margin-top: 0.5rem;
|
||||
$orbit-bullet-margin-bottom: 0.5rem;
|
||||
$orbit-caption-background: rgba($black, 0.5);
|
||||
$orbit-caption-padding: 0.625rem;
|
||||
$orbit-control-background-hover: rgba($black, 0.5);
|
||||
$orbit-control-padding: 0.625rem;
|
||||
$orbit-control-zindex: 10;
|
||||
|
||||
// 26. Pagination
|
||||
// --------------
|
||||
|
||||
$pagination-font-size: rem-calc(14);
|
||||
$pagination-margin-bottom: $global-margin;
|
||||
$pagination-item-color: $black;
|
||||
$pagination-item-padding: rem-calc(3 10);
|
||||
$pagination-item-spacing: rem-calc(1);
|
||||
$pagination-radius: $global-radius;
|
||||
$pagination-item-background-hover: $light-gray;
|
||||
$pagination-item-background-current: $primary-color;
|
||||
$pagination-item-color-current: $white;
|
||||
$pagination-item-color-disabled: $medium-gray;
|
||||
$pagination-ellipsis-color: $black;
|
||||
$pagination-mobile-items: false;
|
||||
$pagination-mobile-current-item: false;
|
||||
$pagination-arrows: true;
|
||||
|
||||
// 27. Progress Bar
|
||||
// ----------------
|
||||
|
||||
$progress-height: 0.625rem;
|
||||
$progress-background: $medium-gray;
|
||||
$progress-margin-bottom: $global-margin;
|
||||
$progress-meter-background: $primary-color;
|
||||
$progress-radius: $global-radius;
|
||||
|
||||
// 28. Responsive Embed
|
||||
// --------------------
|
||||
|
||||
$responsive-embed-margin-bottom: rem-calc(16);
|
||||
$responsive-embed-ratios: (default: 4 by 3,
|
||||
widescreen: 16 by 9);
|
||||
|
||||
// 29. Reveal
|
||||
// ----------
|
||||
|
||||
$reveal-background: $white;
|
||||
$reveal-width: 600px;
|
||||
$reveal-max-width: $global-width;
|
||||
$reveal-padding: $global-padding;
|
||||
$reveal-border: 1px solid $medium-gray;
|
||||
$reveal-radius: $global-radius;
|
||||
$reveal-zindex: 1005;
|
||||
$reveal-overlay-background: rgba($black, 0.45);
|
||||
|
||||
// 30. Slider
|
||||
// ----------
|
||||
|
||||
$slider-width-vertical: 0.3125rem;
|
||||
$slider-transition: all 0.2s ease-in-out;
|
||||
$slider-height: 0.3125rem;
|
||||
$slider-background: $light-gray;
|
||||
$slider-fill-background: $medium-gray;
|
||||
$slider-handle-height: 0.875rem;
|
||||
$slider-handle-width: 0.875rem;
|
||||
$slider-handle-background: $primary-color;
|
||||
$slider-opacity-disabled: 0.25;
|
||||
$slider-radius: $global-radius;
|
||||
|
||||
// 31. Switch
|
||||
// ----------
|
||||
|
||||
$switch-background: $light-gray;
|
||||
$switch-background-active: $primary-color;
|
||||
$switch-height: $space-two;
|
||||
$switch-height-tiny: $space-slab;
|
||||
$switch-height-small: $space-normal;
|
||||
$switch-height-large: $space-large;
|
||||
$switch-radius: $space-large;
|
||||
$switch-margin: $global-margin;
|
||||
$switch-paddle-background: $white;
|
||||
$switch-paddle-offset: $space-micro;
|
||||
$switch-paddle-radius: $space-large;
|
||||
$switch-paddle-transition: all 0.15s ease-out;
|
||||
|
||||
// 32. Table
|
||||
// ---------
|
||||
|
||||
$table-background: transparent;
|
||||
$table-color-scale: 5%;
|
||||
$table-border: 1px solid transparent;
|
||||
$table-padding: rem-calc(8 10 10);
|
||||
$table-hover-scale: 2%;
|
||||
$table-row-hover: darken($table-background, $table-hover-scale);
|
||||
$table-row-stripe-hover: darken($table-background,
|
||||
$table-color-scale + $table-hover-scale);
|
||||
$table-is-striped: false;
|
||||
$table-striped-background: smart-scale($table-background, $table-color-scale);
|
||||
$table-stripe: even;
|
||||
$table-head-background: smart-scale($table-background, $table-color-scale / 2);
|
||||
$table-head-row-hover: darken($table-head-background, $table-hover-scale);
|
||||
$table-foot-background: smart-scale($table-background, $table-color-scale);
|
||||
$table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
|
||||
$table-head-font-color: $body-font-color;
|
||||
$table-foot-font-color: $body-font-color;
|
||||
$show-header-for-stacked: false;
|
||||
|
||||
// 33. Tabs
|
||||
// --------
|
||||
|
||||
$tab-margin: 0;
|
||||
|
||||
$tab-background: transparent;
|
||||
$tab-background-active: transparent;
|
||||
$tab-item-font-size: $font-size-small;
|
||||
$tab-item-background-hover: transparent;
|
||||
$tab-item-padding: $space-one $zero;
|
||||
$tab-color: $primary-color;
|
||||
$tab-active-color: $primary-color;
|
||||
$tab-expand-max: 6;
|
||||
$tab-content-background: transparent;
|
||||
$tab-content-border: transparent;
|
||||
$tab-content-color: foreground($tab-background, $primary-color);
|
||||
$tab-content-padding: 0.625rem;
|
||||
|
||||
// 34. Thumbnail
|
||||
// -------------
|
||||
|
||||
$thumbnail-border: solid 4px $white;
|
||||
$thumbnail-margin-bottom: $global-margin;
|
||||
$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2);
|
||||
$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
|
||||
$thumbnail-transition: box-shadow 200ms ease-out;
|
||||
$thumbnail-radius: $global-radius;
|
||||
|
||||
// 35. Title Bar
|
||||
// -------------
|
||||
|
||||
$titlebar-background: $black;
|
||||
$titlebar-color: $white;
|
||||
$titlebar-padding: 0.3125rem;
|
||||
$titlebar-text-font-weight: bold;
|
||||
$titlebar-icon-color: $white;
|
||||
$titlebar-icon-color-hover: $medium-gray;
|
||||
$titlebar-icon-spacing: 0.15625rem;
|
||||
|
||||
// 36. Tooltip
|
||||
// -----------
|
||||
|
||||
$has-tip-font-weight: $global-weight-bold;
|
||||
$has-tip-border-bottom: dotted 1px $dark-gray;
|
||||
$tooltip-background-color: $black;
|
||||
$tooltip-color: $white;
|
||||
$tooltip-padding: 0.46875rem;
|
||||
$tooltip-font-size: $font-size-mini;
|
||||
$tooltip-pip-width: 0.46875rem;
|
||||
$tooltip-pip-height: $tooltip-pip-width * 0.866;
|
||||
$tooltip-radius: $global-radius;
|
||||
|
||||
// 37. Top Bar
|
||||
// -----------
|
||||
|
||||
$topbar-padding: 0.3125;
|
||||
$topbar-background: $light-gray;
|
||||
$topbar-submenu-background: $topbar-background;
|
||||
$topbar-title-spacing: 0.3125 0.625rem 0.3125 0;
|
||||
$topbar-input-width: 200px;
|
||||
$topbar-unstack-breakpoint: medium;
|
||||
|
||||
|
||||
// Internal variable that contains the flex justifying options
|
||||
$-zf-flex-justify: -zf-flex-justify($global-text-direction);
|
||||
|
||||
$menu-items-padding: $space-one;
|
||||
$xy-grid: false;
|
||||
@@ -2,6 +2,11 @@
|
||||
@apply bg-slate-25 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
@include flex-align(center, middle);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bottom-space-fix {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ $swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
|
||||
@@ -15,13 +15,36 @@
|
||||
@import 'variables';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
@include foundation-prototype-text-utilities;
|
||||
@include foundation-prototype-text-transformation;
|
||||
@include foundation-prototype-text-decoration;
|
||||
@include foundation-prototype-font-styling;
|
||||
@include foundation-prototype-list-style-type;
|
||||
@include foundation-prototype-rounded;
|
||||
@include foundation-prototype-bordered;
|
||||
@include foundation-prototype-shadow;
|
||||
@include foundation-prototype-separator;
|
||||
@include foundation-prototype-overflow;
|
||||
@include foundation-prototype-display;
|
||||
@include foundation-prototype-position;
|
||||
@include foundation-prototype-border-box;
|
||||
@include foundation-prototype-border-none;
|
||||
@include foundation-prototype-sizing;
|
||||
@include foundation-prototype-spacing;
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
@import 'foundation-custom';
|
||||
@import 'rtl';
|
||||
|
||||
@import 'widgets/buttons';
|
||||
@@ -29,11 +52,15 @@
|
||||
@import 'widgets/conversation-card';
|
||||
@import 'widgets/conversation-view';
|
||||
@import 'widgets/forms';
|
||||
@import 'widgets/login';
|
||||
@import 'widgets/modal';
|
||||
@import 'widgets/reply-box';
|
||||
@import 'widgets/report';
|
||||
@import 'widgets/search-box';
|
||||
@import 'widgets/sidemenu';
|
||||
@import 'widgets/snackbar';
|
||||
@import 'widgets/states';
|
||||
@import 'widgets/status-bar';
|
||||
@import 'widgets/tabs';
|
||||
@import 'widgets/woot-tables';
|
||||
|
||||
@@ -42,6 +69,7 @@
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
@import 'utility-helpers';
|
||||
|
||||
.tooltip {
|
||||
|
||||
@@ -10,14 +10,21 @@
|
||||
@import 'variables';
|
||||
|
||||
@import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
|
||||
@include foundation-prototype-spacing;
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
|
||||
@import 'foundation-custom';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/forms';
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
.auth-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Outside login wrapper
|
||||
.login {
|
||||
@include full-height;
|
||||
overflow-y: auto;
|
||||
padding-top: $space-larger * 1.2;
|
||||
|
||||
.login__hero {
|
||||
margin-bottom: $space-larger;
|
||||
|
||||
.hero__logo {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
font-weight: $font-weight-light;
|
||||
margin-top: $space-larger;
|
||||
}
|
||||
|
||||
.hero__sub {
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// Login box
|
||||
.login-box {
|
||||
@include background-white;
|
||||
@include border-normal;
|
||||
@include elegant-card;
|
||||
|
||||
border-radius: $space-smaller;
|
||||
padding: $space-large;
|
||||
|
||||
label {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-default;
|
||||
|
||||
input {
|
||||
font-size: $font-size-default;
|
||||
height: $space-larger;
|
||||
padding: $space-slab;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
height: $space-larger;
|
||||
}
|
||||
}
|
||||
|
||||
.sigin__footer {
|
||||
font-size: $font-size-default;
|
||||
padding: $space-medium;
|
||||
|
||||
> a {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
.modal-mask {
|
||||
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
// @include flex;
|
||||
// @include flex-align(center, middle);
|
||||
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
@@ -43,6 +45,8 @@
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
// @include flex;
|
||||
// @include flex-align($x: flex-end, $y: middle);
|
||||
@apply flex justify-end items-center py-2 px-0 gap-2;
|
||||
|
||||
&.justify-content-end {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
.reply-box__top {
|
||||
.icon {
|
||||
color: var(--slate-500);
|
||||
color: $medium-gray;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-medium;
|
||||
margin-right: $space-small;
|
||||
@@ -34,7 +34,7 @@
|
||||
resize: none;
|
||||
}
|
||||
|
||||
>textarea {
|
||||
> textarea {
|
||||
@include ghost-input();
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
@@ -52,7 +52,7 @@
|
||||
.reply-box__top {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
>input {
|
||||
> input {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
.side-menu {
|
||||
i {
|
||||
margin-right: var(--space-smaller);
|
||||
min-width: var(--space-two);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
z-index: 1024 - 1;
|
||||
|
||||
//logo
|
||||
.logo {
|
||||
img {
|
||||
max-height: 108px;
|
||||
padding: $woot-logo-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.nested {
|
||||
a {
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: var(--space-micro);
|
||||
margin-top: var(--space-micro);
|
||||
|
||||
.inbox-icon {
|
||||
display: inline-block;
|
||||
margin-right: var(--space-micro);
|
||||
min-width: var(--space-normal);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bottom-nav
|
||||
.bottom-nav {
|
||||
@include flex;
|
||||
@include space-between-column;
|
||||
@include border-normal-top;
|
||||
flex-direction: column;
|
||||
padding: var(--space-one) var(--space-normal) var(--space-one) var(--space-one);
|
||||
padding: var(--space-one) var(--space-normal) var(--space-one)
|
||||
var(--space-one);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
class="attribute-name"
|
||||
:class="{ error: v$.editedValue.$error }"
|
||||
:class="{ error: $v.editedValue.$error }"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
@@ -39,8 +39,8 @@
|
||||
:type="inputType"
|
||||
class="input-group-field"
|
||||
autofocus="true"
|
||||
:class="{ error: v$.editedValue.$error }"
|
||||
@blur="v$.editedValue.$touch"
|
||||
:class="{ error: $v.editedValue.$error }"
|
||||
@blur="$v.editedValue.$touch"
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div class="input-group-button">
|
||||
@@ -122,13 +122,12 @@
|
||||
|
||||
<script>
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { required, url } from '@vuelidate/validators';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
import customAttributeMixin from '../mixins/customAttributeMixin';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -150,9 +149,6 @@ export default {
|
||||
attributeKey: { type: String, required: true },
|
||||
contactId: { type: Number, default: null },
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
@@ -210,13 +206,13 @@ export default {
|
||||
return this.isAttributeTypeLink ? 'url' : this.attributeType;
|
||||
},
|
||||
shouldShowErrorMessage() {
|
||||
return this.v$.editedValue.$error;
|
||||
return this.$v.editedValue.$error;
|
||||
},
|
||||
errorMessage() {
|
||||
if (this.v$.editedValue.url) {
|
||||
if (this.$v.editedValue.url) {
|
||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
||||
}
|
||||
if (!this.v$.editedValue.regexValidation) {
|
||||
if (!this.$v.editedValue.regexValidation) {
|
||||
return this.regexCue
|
||||
? this.regexCue
|
||||
: this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
|
||||
@@ -284,8 +280,8 @@ export default {
|
||||
this.attributeType === 'date'
|
||||
? parseISO(this.editedValue)
|
||||
: this.editedValue;
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.isEditing = false;
|
||||
@@ -306,14 +302,11 @@ export default {
|
||||
.checkbox-wrap {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply my-0 mr-2 ml-0;
|
||||
}
|
||||
|
||||
.attribute-name {
|
||||
@apply w-full text-slate-800 dark:text-slate-100;
|
||||
|
||||
&.error {
|
||||
@apply text-red-400 dark:text-red-500;
|
||||
}
|
||||
@@ -330,7 +323,6 @@ export default {
|
||||
.value {
|
||||
@apply bg-slate-50 dark:bg-slate-700 mb-0;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@apply block;
|
||||
}
|
||||
@@ -340,12 +332,10 @@ export default {
|
||||
::v-deep {
|
||||
.selector-wrap {
|
||||
@apply m-0 top-1;
|
||||
|
||||
.selector-name {
|
||||
@apply ml-0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply ml-0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<banner
|
||||
v-if="shouldShowBanner"
|
||||
color-scheme="alert"
|
||||
:banner-message="bannerMessage"
|
||||
:action-button-label="actionButtonMessage"
|
||||
action-button-icon="mail"
|
||||
has-action-button
|
||||
@click="resendVerificationEmail"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin, alertMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
bannerMessage() {
|
||||
return this.$t('APP_GLOBAL.EMAIL_VERIFICATION_PENDING');
|
||||
},
|
||||
actionButtonMessage() {
|
||||
return this.$t('APP_GLOBAL.RESEND_VERIFICATION_MAIL');
|
||||
},
|
||||
shouldShowBanner() {
|
||||
return !this.currentUser.confirmed;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resendVerificationEmail() {
|
||||
this.$store.dispatch('resendConfirmation');
|
||||
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,29 +1,29 @@
|
||||
/* eslint no-plusplus: 0 */
|
||||
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
|
||||
import Bar from './widgets/chart/BarChart';
|
||||
import Button from './ui/WootButton.vue';
|
||||
import Code from './Code.vue';
|
||||
import ColorPicker from './widgets/ColorPicker.vue';
|
||||
import Button from './ui/WootButton';
|
||||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
import ContextMenu from './ui/ContextMenu.vue';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import FeatureToggle from './widgets/FeatureToggle.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import FeatureToggle from './widgets/FeatureToggle';
|
||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import PhoneInput from './widgets/forms/PhoneInput.vue';
|
||||
import Label from './ui/Label.vue';
|
||||
import LoadingState from './widgets/LoadingState.vue';
|
||||
import Modal from './Modal.vue';
|
||||
import ModalHeader from './ModalHeader.vue';
|
||||
import ReportStatsCard from './widgets/ReportStatsCard.vue';
|
||||
import SidemenuIcon from './SidemenuIcon.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import SubmitButton from './buttons/FormSubmitButton.vue';
|
||||
import Label from './ui/Label';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
import Modal from './Modal';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ReportStatsCard from './widgets/ReportStatsCard';
|
||||
import SidemenuIcon from './SidemenuIcon';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import SubmitButton from './buttons/FormSubmitButton';
|
||||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem.vue';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
|
||||
@@ -39,6 +39,7 @@ const settings = accountId => ({
|
||||
'settings_teams_finish',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'sla_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -158,6 +159,15 @@ const settings = accountId => ({
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'key',
|
||||
label: 'SLA',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||
toStateName: 'sla_list',
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
beta: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
|
||||
<form class="flex flex-col w-full" @submit.prevent="addAccount">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.accountName.$error }">
|
||||
<label :class="{ error: $v.accountName.$error }">
|
||||
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="accountName"
|
||||
type="text"
|
||||
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.accountName.$touch"
|
||||
@input="$v.accountName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -33,8 +33,8 @@
|
||||
<div class="w-full">
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
v$.accountName.$invalid ||
|
||||
v$.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||
@@ -49,10 +49,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
@@ -66,9 +65,6 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accountName: '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="banner flex items-center h-12 gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center"
|
||||
class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white"
|
||||
:class="bannerClasses"
|
||||
>
|
||||
<span class="banner-message">
|
||||
@@ -18,7 +18,7 @@
|
||||
<woot-button
|
||||
v-if="hasActionButton"
|
||||
size="tiny"
|
||||
icon="arrow-right"
|
||||
:icon="actionButtonIcon"
|
||||
:variant="actionButtonVariant"
|
||||
color-scheme="primary"
|
||||
class-names="banner-action__button"
|
||||
@@ -67,6 +67,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionButtonIcon: {
|
||||
type: String,
|
||||
default: 'arrow-right',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
export default {
|
||||
name: 'WootTabs',
|
||||
props: {
|
||||
index: {
|
||||
@@ -75,25 +73,19 @@ export default defineComponent({
|
||||
});
|
||||
const leftButton = this.createScrollButton(createElement, 'left');
|
||||
const rightButton = this.createScrollButton(createElement, 'right');
|
||||
return createElement(
|
||||
'div',
|
||||
{
|
||||
class: {
|
||||
return (
|
||||
<div
|
||||
class={{
|
||||
'tabs--container--with-border': this.border,
|
||||
'tabs--container': true,
|
||||
},
|
||||
},
|
||||
[
|
||||
leftButton,
|
||||
createElement(
|
||||
'ul',
|
||||
{
|
||||
class: { tabs: true, 'tabs--with-scroll': this.hasScroll },
|
||||
},
|
||||
Tabs
|
||||
),
|
||||
rightButton,
|
||||
]
|
||||
}}
|
||||
>
|
||||
{leftButton}
|
||||
<ul class={{ tabs: true, 'tabs--with-scroll': this.hasScroll }}>
|
||||
{Tabs}
|
||||
</ul>
|
||||
{rightButton}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
<woot-input
|
||||
v-model="value"
|
||||
type="text"
|
||||
:class="{ error: v$.value.$error }"
|
||||
:class="{ error: $v.value.$error }"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.KEY_PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.value.$touch"
|
||||
@blur="$v.value.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
|
||||
@@ -27,7 +27,7 @@
|
||||
<woot-button variant="clear" @click.prevent="onDismiss">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
|
||||
</woot-button>
|
||||
<woot-button :is-disabled="v$.value.$invalid">
|
||||
<woot-button :is-disabled="$v.value.$invalid">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
@@ -37,19 +37,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
mixins: [aiMixin, alertMixin, uiSettingsMixin],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
|
||||
@@ -53,8 +53,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'dashboard/assets/scss/mixins';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.colorpicker {
|
||||
position: relative;
|
||||
|
||||
@@ -705,11 +705,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@chatwoot/prosemirror-schema/src/styles/base.scss';
|
||||
@import '~@chatwoot/prosemirror-schema/src/styles/base.scss';
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
@apply flex flex-col;
|
||||
|
||||
.ProseMirror-menubar {
|
||||
min-height: var(--space-two) !important;
|
||||
@apply -ml-2.5 pb-0 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100;
|
||||
@@ -718,7 +717,6 @@ export default {
|
||||
@apply bg-slate-75 dark:bg-slate-800;
|
||||
}
|
||||
}
|
||||
|
||||
> .ProseMirror {
|
||||
@apply p-0 break-words text-slate-800 dark:text-slate-100;
|
||||
|
||||
@@ -755,7 +753,7 @@ export default {
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
@apply z-50 bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800;
|
||||
@apply z-[9999] bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800 shadow-lg;
|
||||
|
||||
h5 {
|
||||
@apply dark:text-slate-25 text-slate-800;
|
||||
|
||||
+4
-8
@@ -41,7 +41,7 @@
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:grouped-filters="true"
|
||||
:v="v$.appliedFilters.$each[i]"
|
||||
:v="$v.appliedFilters.$each[i]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, requiredIf } from '@vuelidate/validators';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import FilterInputBox from '../FilterInput/Index.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
@@ -89,7 +89,6 @@ import { filterAttributeGroups } from './advancedFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -118,9 +117,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
required,
|
||||
@@ -351,8 +347,8 @@ export default {
|
||||
}
|
||||
},
|
||||
submitFilterQuery() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) return;
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) return;
|
||||
this.$store.dispatch(
|
||||
'setConversationFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<conversation-header
|
||||
v-if="currentChat.id"
|
||||
:chat="currentChat"
|
||||
:is-inbox-view="isInboxView"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:show-back-button="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
@@ -30,6 +31,7 @@
|
||||
<messages-view
|
||||
v-if="currentChat.id"
|
||||
:inbox-id="inboxId"
|
||||
:is-inbox-view="isInboxView"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
@@ -80,6 +82,10 @@ export default {
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
isInboxView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0"
|
||||
class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0 rtl:relative rtl:left-6"
|
||||
:class="{ 'justify-end': isContactPanelOpen }"
|
||||
>
|
||||
<more-actions :conversation-id="currentChat.id" />
|
||||
@@ -86,7 +86,8 @@ import MoreActions from './MoreActions.vue';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -109,6 +110,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -123,6 +128,9 @@ export default {
|
||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||
name,
|
||||
} = this.$route;
|
||||
if (this.isInboxView) {
|
||||
return frontendURL(`accounts/${accountId}/inbox`);
|
||||
}
|
||||
return conversationListPageURL({
|
||||
accountId,
|
||||
inboxId,
|
||||
@@ -150,7 +158,7 @@ export default {
|
||||
if (snoozedUntil) {
|
||||
return `${this.$t(
|
||||
'CONVERSATION.HEADER.SNOOZED_UNTIL'
|
||||
)} ${conversationReopenTime(snoozedUntil)}`;
|
||||
)} ${snoozedReopenTime(snoozedUntil)}`;
|
||||
}
|
||||
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
|
||||
},
|
||||
|
||||
@@ -45,14 +45,14 @@
|
||||
}}</label>
|
||||
</div>
|
||||
<div v-if="sentToOtherEmailAddress" class="w-[50%] mt-1">
|
||||
<label :class="{ error: v$.email.$error }">
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="text"
|
||||
:placeholder="$t('EMAIL_TRANSCRIPT.FORM.EMAIL.PLACEHOLDER')"
|
||||
@input="v$.email.$touch"
|
||||
@input="$v.email.$touch"
|
||||
/>
|
||||
<span v-if="v$.email.$error" class="message">
|
||||
<span v-if="$v.email.$error" class="message">
|
||||
{{ $t('EMAIL_TRANSCRIPT.FORM.EMAIL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
@@ -73,10 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength, email } from '@vuelidate/validators';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
@@ -89,9 +87,6 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
@@ -113,7 +108,7 @@ export default {
|
||||
isFormValid() {
|
||||
if (this.selectedType) {
|
||||
if (this.sentToOtherEmailAddress) {
|
||||
return !!this.email && !this.v$.email.$error;
|
||||
return !!this.email && !this.$v.email.$error;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
||||
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
||||
:class="
|
||||
isInboxView ? 'top-52 md:top-40' : 'top-[9.5rem] md:top-[6.25rem]'
|
||||
"
|
||||
:icon="isRightOrLeftIcon"
|
||||
@click="onToggleContactPanel"
|
||||
/>
|
||||
@@ -142,6 +145,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
||||
@@ -164,7 +164,7 @@ import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel
|
||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
@@ -198,7 +198,7 @@ import {
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -1208,7 +1208,6 @@ export default {
|
||||
@apply bg-yellow-50 dark:bg-yellow-200;
|
||||
}
|
||||
}
|
||||
|
||||
.send-button {
|
||||
@apply mb-0;
|
||||
}
|
||||
@@ -1229,7 +1228,6 @@ export default {
|
||||
|
||||
.emoji-dialog--rtl {
|
||||
@apply left-[unset] -right-80;
|
||||
|
||||
&::before {
|
||||
transform: rotate(90deg);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="toEmails">
|
||||
<div class="input-group small" :class="{ error: v$.toEmailsVal.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.toEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.TO') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="v$.toEmailsVal.$model"
|
||||
v-model.trim="$v.toEmailsVal.$model"
|
||||
type="text"
|
||||
:class="{ error: v$.toEmailsVal.$error }"
|
||||
:class="{ error: $v.toEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
@@ -17,15 +17,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: v$.ccEmailsVal.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.ccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="v$.ccEmailsVal.$model"
|
||||
v-model.trim="$v.ccEmailsVal.$model"
|
||||
type="text"
|
||||
:class="{ error: v$.ccEmailsVal.$error }"
|
||||
:class="{ error: $v.ccEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
@@ -39,20 +39,20 @@
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<span v-if="v$.ccEmailsVal.$error" class="message">
|
||||
<span v-if="$v.ccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showBcc" class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: v$.bccEmailsVal.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.bccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="v$.bccEmailsVal.$model"
|
||||
v-model.trim="$v.bccEmailsVal.$model"
|
||||
type="text"
|
||||
:class="{ error: v$.bccEmailsVal.$error }"
|
||||
:class="{ error: $v.bccEmailsVal.$error }"
|
||||
:placeholder="
|
||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||
"
|
||||
@@ -60,7 +60,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="v$.bccEmailsVal.$error" class="message">
|
||||
<span v-if="$v.bccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
this.showBcc = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.v$.$touch();
|
||||
this.$v.$touch();
|
||||
this.$emit('update:bccEmails', this.bccEmailsVal);
|
||||
this.$emit('update:ccEmails', this.ccEmailsVal);
|
||||
this.$emit('update:toEmails', this.toEmailsVal);
|
||||
@@ -149,14 +149,12 @@ export default {
|
||||
.input-group-wrap .message {
|
||||
@apply text-sm text-red-500 dark:text-red-500;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@apply border-b border-solid border-slate-75 dark:border-slate-700 my-1;
|
||||
|
||||
.input-group-label {
|
||||
@apply border-transparent bg-transparent text-xs font-semibold pl-0;
|
||||
}
|
||||
|
||||
.input-group-field::v-deep input {
|
||||
@apply mb-0 border-transparent;
|
||||
}
|
||||
@@ -164,7 +162,6 @@ export default {
|
||||
|
||||
.input-group.error {
|
||||
@apply border-b-red-500 dark:border-b-red-500;
|
||||
|
||||
.input-group-label {
|
||||
@apply text-red-500 dark:text-red-500;
|
||||
}
|
||||
|
||||
+1
-8
@@ -45,9 +45,7 @@ const allKeysRequired = value => {
|
||||
const keys = Object.keys(value);
|
||||
return keys.every(key => value[key]);
|
||||
};
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { requiredIf } from 'vuelidate/lib/validators';
|
||||
export default {
|
||||
props: {
|
||||
template: {
|
||||
@@ -55,9 +53,6 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
validations: {
|
||||
processedParams: {
|
||||
requiredIfKeysPresent: requiredIf('variables'),
|
||||
@@ -154,11 +149,9 @@ footer {
|
||||
@apply ml-2.5;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
|
||||
}
|
||||
|
||||
.template-input {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { email as emailValidator } from '@vuelidate/validators';
|
||||
import emailValidator from 'vuelidate/lib/validators/email';
|
||||
|
||||
export const validEmailsByComma = value => {
|
||||
if (!value.length) return true;
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('MoveActions', () => {
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This conversation is muted for 6 hours',
|
||||
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe('MoveActions', () => {
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This conversation is unmuted',
|
||||
'This contact is unblocked successfully.',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<woot-input
|
||||
v-model="value"
|
||||
type="text"
|
||||
:class="{ error: v$.value.$error }"
|
||||
:class="{ error: $v.value.$error }"
|
||||
:placeholder="confirmPlaceHolderText"
|
||||
@blur="v$.value.$touch"
|
||||
@blur="$v.value.$touch"
|
||||
/>
|
||||
<div class="button-wrapper">
|
||||
<woot-button color-scheme="alert" :is-disabled="v$.value.$invalid">
|
||||
<woot-button color-scheme="alert" :is-disabled="$v.value.$invalid">
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
<woot-button class="clear" @click.prevent="closeModal">
|
||||
@@ -23,14 +23,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import Modal from '../../Modal.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -61,9 +61,6 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
|
||||
@@ -46,5 +46,18 @@ export default {
|
||||
},
|
||||
EXAMPLE_URL: 'https://example.com',
|
||||
EXAMPLE_WEBHOOK_URL: 'https://example/api/webhook',
|
||||
INBOX_SORT_BY: {
|
||||
NEWEST: 'desc',
|
||||
OLDEST: 'asc',
|
||||
},
|
||||
INBOX_DISPLAY_BY: {
|
||||
SNOOZED: 'snoozed',
|
||||
READ: 'read',
|
||||
},
|
||||
INBOX_FILTER_TYPE: {
|
||||
STATUS: 'status',
|
||||
TYPE: 'type',
|
||||
SORT_ORDER: 'sort_order',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
@@ -17,4 +17,6 @@ export const FEATURE_FLAGS = {
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
AUDIT_LOGS: 'audit_logs',
|
||||
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
||||
INBOX_VIEW: 'inbox_view',
|
||||
SLA: 'sla',
|
||||
};
|
||||
|
||||
@@ -102,3 +102,18 @@ export const OPEN_AI_EVENTS = Object.freeze({
|
||||
export const GENERAL_EVENTS = Object.freeze({
|
||||
COMMAND_BAR: 'Used commandbar',
|
||||
});
|
||||
|
||||
export const INBOX_EVENTS = Object.freeze({
|
||||
OPEN_CONVERSATION_VIA_INBOX: 'Opened conversation via inbox',
|
||||
MARK_NOTIFICATION_AS_READ: 'Marked notification as read',
|
||||
MARK_ALL_NOTIFICATIONS_AS_READ: 'Marked all notifications as read',
|
||||
MARK_NOTIFICATION_AS_UNREAD: 'Marked notification as unread',
|
||||
DELETE_NOTIFICATION: 'Deleted notification',
|
||||
DELETE_ALL_NOTIFICATIONS: 'Deleted all notifications',
|
||||
});
|
||||
|
||||
export const SLA_EVENTS = Object.freeze({
|
||||
CREATE: 'Created an SLA',
|
||||
UPDATE: 'Updated an SLA',
|
||||
DELETED: 'Deleted an SLA',
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'conversation.mentioned': this.onConversationMentioned,
|
||||
'notification.created': this.onNotificationCreated,
|
||||
'notification.deleted': this.onNotificationDeleted,
|
||||
'notification.updated': this.onNotificationUpdated,
|
||||
'first.reply.created': this.onFirstReplyCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
'conversation.updated': this.onConversationUpdated,
|
||||
@@ -200,6 +201,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
this.app.$store.dispatch('notifications/deleteNotification', data);
|
||||
};
|
||||
|
||||
onNotificationUpdated = data => {
|
||||
this.app.$store.dispatch('notifications/updateNotification', data);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onFirstReplyCreated = () => {
|
||||
bus.$emit('fetch_overview_reports');
|
||||
|
||||
@@ -129,7 +129,13 @@ export const agentList = agents => [
|
||||
...(agents || []),
|
||||
];
|
||||
|
||||
export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||
export const getActionOptions = ({
|
||||
agents,
|
||||
teams,
|
||||
labels,
|
||||
slaPolicies,
|
||||
type,
|
||||
}) => {
|
||||
const actionsMap = {
|
||||
assign_agent: agentList(agents),
|
||||
assign_team: teams,
|
||||
@@ -137,6 +143,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
change_priority: PRIORITY_CONDITION_VALUES,
|
||||
add_sla: slaPolicies,
|
||||
};
|
||||
return actionsMap[type];
|
||||
};
|
||||
|
||||
@@ -86,3 +86,6 @@ export const getConversationDashboardRoute = routeName => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const isAInboxViewRoute = routeName =>
|
||||
['inbox_view_conversation'].includes(routeName);
|
||||
|
||||
@@ -55,7 +55,7 @@ export const findSnoozeTime = (snoozeType, currentDate = new Date()) => {
|
||||
|
||||
return parsedDate ? getUnixTime(parsedDate) : null;
|
||||
};
|
||||
export const conversationReopenTime = snoozedUntil => {
|
||||
export const snoozedReopenTime = snoozedUntil => {
|
||||
if (!snoozedUntil) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isAConversationRoute,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
} from '../routeHelpers';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
@@ -134,3 +135,10 @@ describe('getConversationDashboardRoute', () => {
|
||||
expect(getConversationDashboardRoute('non_existent_route')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAInboxViewRoute', () => {
|
||||
it('returns true if inbox view route name is provided', () => {
|
||||
expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true);
|
||||
expect(isAInboxViewRoute('inbox_conversation')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
findSnoozeTime,
|
||||
conversationReopenTime,
|
||||
snoozedReopenTime,
|
||||
findStartOfNextWeek,
|
||||
findStartOfNextMonth,
|
||||
findNextDay,
|
||||
@@ -88,13 +88,13 @@ describe('#Snooze Helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('conversationReopenTime', () => {
|
||||
describe('snoozedReopenTime', () => {
|
||||
it('should return nil if snoozedUntil is nil', () => {
|
||||
expect(conversationReopenTime(null)).toEqual(null);
|
||||
expect(snoozedReopenTime(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return formatted date if snoozedUntil is not nil', () => {
|
||||
expect(conversationReopenTime('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
expect(snoozedReopenTime('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
'7 Jun, 9.00am'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,17 @@
|
||||
"PLACEHOLDER": "Enter custom attribute key",
|
||||
"ERROR": "Key is required",
|
||||
"IN_VALID": "Invalid key"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
@@ -93,6 +104,17 @@
|
||||
"EMPTY_RESULT": {
|
||||
"404": "There are no custom attributes created",
|
||||
"NOT_FOUND": "There are no custom attributes configured"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"MUTE_CONTACT": "Mute Conversation",
|
||||
"UNMUTE_CONTACT": "Unmute Conversation",
|
||||
"MUTED_SUCCESS": "This conversation is muted for 6 hours",
|
||||
"UNMUTED_SUCCESS": "This conversation is unmuted",
|
||||
"MUTE_CONTACT": "Block Contact",
|
||||
"UNMUTE_CONTACT": "Unblock Contact",
|
||||
"MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.",
|
||||
"UNMUTED_SUCCESS": "This contact is unblocked successfully.",
|
||||
"SEND_TRANSCRIPT": "Send Transcript",
|
||||
"EDIT_LABEL": "Edit",
|
||||
"SIDEBAR_SECTIONS": {
|
||||
@@ -79,7 +79,13 @@
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
"ERROR_MESSAGE": "There was an error, please try again",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Export Contacts",
|
||||
"MESSAGE": "Are you sure you want to export all contacts?",
|
||||
"YES": "Yes, Export",
|
||||
"NO": "No, Cancel"
|
||||
}
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
"CONFIRM": {
|
||||
@@ -339,7 +345,8 @@
|
||||
},
|
||||
"VALIDATIONS": {
|
||||
"REQUIRED": "Valid value is required",
|
||||
"INVALID_URL": "Invalid URL"
|
||||
"INVALID_URL": "Invalid URL",
|
||||
"INVALID_INPUT": "Invalid Input"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user