Compare commits

..
Author SHA1 Message Date
Pranav Raj S 46b91ad840 Running version 2024-01-27 23:33:15 -08:00
Pranav Raj S ed8b58ad10 Remove foundation 2024-01-27 23:25:10 -08:00
Pranav Raj S 4928f28c64 Remove ionicons 2024-01-27 23:23:27 -08:00
Pranav Raj S 30539c27f2 Merge branch 'develop' into vite-ruby-migration 2024-01-27 23:21:44 -08:00
Pranav Raj S 2e747e7fa6 Remove webpack 2024-01-25 12:43:39 -08:00
Pranav Raj S a1b6b0c06c Running dashboard, help center 2024-01-25 12:32:50 -08:00
Pranav Raj S 1fe3960285 Upgrade @vuelidate to 2.x 2024-01-25 12:15:21 -08:00
Pranav Raj S c33c844039 Initial Vite Commit 2024-01-25 11:56:22 -08:00
1564 changed files with 10319 additions and 38762 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
name: yarn name: yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
# Store yarn / webpacker cache # Store yarn cache
- save_cache: - save_cache:
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
paths: paths:
+4 -4
View File
@@ -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 '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/SMTP_ADDRESS/ 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 "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env sed -i -e "/DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
# uncomment the webpacker env variable # uncomment the dev server env variable
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env sed -i -e '/DEV_SERVER_PUBLIC/s/^# //' .env
# fix the error with webpacker # fix the error with dev server
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
# codespaces make the ports public # codespaces make the ports public
+2 -1
View File
@@ -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 # if you want to use letter_opener for local emails
# LETTER_OPENER=true # LETTER_OPENER=true
# meant to be used in github codespaces # meant to be used in github codespaces
# WEBPACKER_DEV_SERVER_PUBLIC= # DEV_SERVER_PUBLIC=
# If you want to use official mobile app, # If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server # the notifications would be relayed via a Chatwoot server
@@ -254,6 +254,7 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path # Sentiment analysis model file path
SENTIMENT_FILE_PATH= SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations # Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes # Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed # contact_inboxes with no conversation older than 90 days will be removed
+2 -10
View File
@@ -7,11 +7,10 @@ module.exports = {
'plugin:cypress/recommended', 'plugin:cypress/recommended',
], ],
parserOptions: { parserOptions: {
parser: '@babel/eslint-parser', ecmaVersion: 2022,
ecmaVersion: 2020,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['html', 'prettier', 'babel'], plugins: ['html', 'prettier'],
rules: { rules: {
'prettier/prettier': ['error'], 'prettier/prettier': ['error'],
camelcase: 'off', camelcase: 'off',
@@ -56,13 +55,6 @@ module.exports = {
'import/extensions': ['off'], 'import/extensions': ['off'],
'no-console': 'error', 'no-console': 'error',
}, },
settings: {
'import/resolver': {
webpack: {
config: 'config/webpack/resolve.js',
},
},
},
env: { env: {
browser: true, browser: true,
jest: true, jest: true,
-7
View File
@@ -1,7 +0,0 @@
## 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
-45
View File
@@ -1,45 +0,0 @@
## github action to check deployment success
## curl the deployment url and check for 200 status
## deployment url will be of the form chatwoot-pr-<pr_number>.herokuapp.com
name: Deploy Check
on:
pull_request:
jobs:
deployment_check:
name: Check Deployment
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Print Deployment URL
run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com"
- name: Check Deployment Status
run: |
max_attempts=10
attempt=1
status_code=0
echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..."
sleep 600
while [ $attempt -le $max_attempts ]; do
response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
status_code=$(echo $response | head -n 1)
if [ $status_code -eq 200 ]; then
body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then
echo "Deployment successful"
exit 0
else
echo "Deployment status unknown, retrying in 3 minutes..."
sleep 180
fi
else
echo "Waiting for review app to be ready, retrying in 3 minutes..."
sleep 180
attempt=$((attempt + 1))
fi
done
echo "Deployment failed after $max_attempts attempts"
exit 1
fi
@@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Check for log lines and calculate percentage - name: Check for log lines and calculate percentage
run: | run: |
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
# curl http://localhost:3000/api # curl http://localhost:3000/api
- name: Upload chatwoot setup log file as an artifact - name: Upload chatwoot setup log file as an artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
if: always() if: always()
with: with:
name: chatwoot-setup-log-file name: chatwoot-setup-log-file
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
+3 -3
View File
@@ -41,7 +41,7 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -50,7 +50,7 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 20
cache: yarn cache: yarn
@@ -80,7 +80,7 @@ jobs:
NODE_OPTIONS: --openssl-legacy-provider NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder - name: Upload rails log folder
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
if: always() if: always()
with: with:
name: rails-log-folder name: rails-log-folder
+3 -4
View File
@@ -40,7 +40,7 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -49,7 +49,7 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 20
cache: yarn cache: yarn
@@ -73,12 +73,11 @@ jobs:
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \ spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \ spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \ spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
--profile=10 \ --profile=10 \
--format documentation --format documentation
- name: Upload rails log folder - name: Upload rails log folder
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
if: always() if: always()
with: with:
name: rails-log-folder name: rails-log-folder
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -19,7 +19,7 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 20
cache: 'yarn' cache: 'yarn'
+8
View File
@@ -76,3 +76,11 @@ yarn-debug.log*
.yarn-integrity .yarn-integrity
/storybook-static /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
+70 -23
View File
@@ -2,8 +2,7 @@ require:
- rubocop-performance - rubocop-performance
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- ./rubocop/use_from_email.rb inherit_from: .rubocop_todo.yml
- ./rubocop/custom_cop_location.rb
Layout/LineLength: Layout/LineLength:
Max: 150 Max: 150
@@ -13,8 +12,7 @@ Metrics/ClassLength:
Exclude: Exclude:
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
Metrics/MethodLength:
Max: 19
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 25 Max: 25
Style/Documentation: Style/Documentation:
@@ -52,7 +50,6 @@ Lint/OrAssignmentToConstant:
Exclude: Exclude:
- 'lib/redis/config.rb' - 'lib/redis/config.rb'
Metrics/BlockLength: Metrics/BlockLength:
Max: 30
Exclude: Exclude:
- spec/**/* - spec/**/*
- '**/routes.rb' - '**/routes.rb'
@@ -105,31 +102,84 @@ RSpec/FactoryBot/SyntaxMethods:
Enabled: false Enabled: false
Naming/VariableNumber: Naming/VariableNumber:
Enabled: false Enabled: false
Naming/MemoizedInstanceVariableName: Metrics/MethodLength:
Exclude: Exclude:
- 'app/models/message.rb' - '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'
Style/GuardClause: Style/GuardClause:
Exclude: Exclude:
- 'app/builders/account_builder.rb' - 'app/builders/account_builder.rb'
- 'app/models/attachment.rb' - 'app/models/attachment.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize: Metrics/AbcSize:
Max: 26
Exclude: Exclude:
- 'app/controllers/concerns/auth_helper.rb' - 'app/controllers/concerns/auth_helper.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
Metrics/CyclomaticComplexity:
Max: 7
Exclude:
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Rails/ReversibleMigration:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161110102609_removeinboxid.rb'
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
- 'db/migrate/20191020085608_rename_old_tables.rb'
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
Rails/BulkChangeTable:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20200121190901_create_account_users.rb'
- 'db/migrate/20170211092540_notnullableusers.rb'
- 'db/migrate/20170403095203_contactadder.rb'
- 'db/migrate/20170406104018_add_default_status_conv.rb'
- 'db/migrate/20170511134418_latlong.rb'
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
Rails/UniqueValidationWithoutIndex: Rails/UniqueValidationWithoutIndex:
Exclude: Exclude:
- 'app/models/channel/twitter_profile.rb' - 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb' - 'app/models/webhook.rb'
- 'app/models/contact.rb' - 'app/models/contact.rb'
- 'app/models/integrations/hook.rb' - 'app/models/integrations/hook.rb'
- 'app/models/canned_response.rb'
- 'app/models/telegram_bot.rb'
Rails/RenderInline: Rails/RenderInline:
Exclude: Exclude:
- 'app/controllers/swagger_controller.rb' - 'app/controllers/swagger_controller.rb'
Performance/CollectionLiteralInLoop:
Exclude:
- 'db/migrate/20210315101919_enable_email_channel.rb'
Rails/ThreeStateBooleanColumn: Rails/ThreeStateBooleanColumn:
Exclude: Exclude:
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
- 'db/migrate/20200606132552_create_labels.rb'
- 'db/migrate/20201027135006_create_working_hours.rb'
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
- 'db/migrate/20210114202310_create_teams.rb'
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
- 'db/migrate/20210428135041_add_campaigns.rb'
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
- 'db/migrate/20230503101201_create_sla_policies.rb' - 'db/migrate/20230503101201_create_sla_policies.rb'
RSpec/IndexedLet: RSpec/IndexedLet:
Enabled: false Enabled: false
@@ -137,21 +187,9 @@ RSpec/NamedSubject:
Enabled: false Enabled: false
# we should bring this down # we should bring this down
RSpec/MultipleExpectations:
Max: 7
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 14 Max: 14
# custom rules
UseFromEmail:
Enabled: true
Exclude:
- 'app/models/user.rb'
- 'app/models/contact.rb'
CustomCopLocation:
Enabled: true
AllCops: AllCops:
NewCops: enable NewCops: enable
Exclude: Exclude:
@@ -165,4 +203,13 @@ AllCops:
- 'config/environments/**/*' - 'config/environments/**/*'
- 'tmp/**/*' - 'tmp/**/*'
- 'storage/**/*' - 'storage/**/*'
- 'db/migrate/20230426130150_init_schema.rb' - '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
+287
View File
@@ -0,0 +1,287 @@
# 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'
+1 -3
View File
@@ -2,7 +2,6 @@ import { addDecorator } from '@storybook/vue';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import VueDOMPurifyHTML from 'vue-dompurify-html'; import VueDOMPurifyHTML from 'vue-dompurify-html';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
@@ -14,7 +13,6 @@ import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
import '../app/javascript/dashboard/assets/scss/storybook.scss'; import '../app/javascript/dashboard/assets/scss/storybook.scss';
Vue.use(VueI18n); Vue.use(VueI18n);
Vue.use(Vuelidate);
Vue.use(WootUiKit); Vue.use(WootUiKit);
Vue.use(Vuex); Vue.use(Vuex);
Vue.use(VueDOMPurifyHTML, domPurifyConfig); Vue.use(VueDOMPurifyHTML, domPurifyConfig);
@@ -32,7 +30,7 @@ addDecorator(() => ({
template: '<story/>', template: '<story/>',
i18n: i18nConfig, i18n: i18nConfig,
store, store,
beforeCreate: function() { beforeCreate: function () {
this.$root._i18n = this.$i18n; this.$root._i18n = this.$i18n;
}, },
})); }));
+3 -3
View File
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
ruby '3.2.2' ruby '3.2.2'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7.0.8.1' gem 'rails', '~> 7.0.8.0'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@@ -64,7 +64,7 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'webpacker' gem 'vite_rails'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
+82 -80
View File
@@ -33,70 +33,70 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.8.1) actioncable (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.1) actionmailbox (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
activejob (= 7.0.8.1) activejob (= 7.0.8)
activerecord (= 7.0.8.1) activerecord (= 7.0.8)
activestorage (= 7.0.8.1) activestorage (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.8.1) actionmailer (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
actionview (= 7.0.8.1) actionview (= 7.0.8)
activejob (= 7.0.8.1) activejob (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (7.0.8.1) actionpack (7.0.8)
actionview (= 7.0.8.1) actionview (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
rack (~> 2.0, >= 2.2.4) rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.1) actiontext (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
activerecord (= 7.0.8.1) activerecord (= 7.0.8)
activestorage (= 7.0.8.1) activestorage (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.8.1) actionview (7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (7.0.8.1) activejob (7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.8.1) activemodel (7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
activerecord (7.0.8.1) activerecord (7.0.8)
activemodel (= 7.0.8.1) activemodel (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
activerecord-import (1.4.1) activerecord-import (1.4.1)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (7.0.8.1) activestorage (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
activejob (= 7.0.8.1) activejob (= 7.0.8)
activerecord (= 7.0.8.1) activerecord (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (7.0.8.1) activesupport (7.0.8)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@@ -216,6 +216,7 @@ GEM
railties (>= 3.2) railties (>= 3.2)
down (5.4.0) down (5.4.0)
addressable (~> 2.8) addressable (~> 2.8)
dry-cli (1.0.0)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
elastic-apm (4.6.2) elastic-apm (4.6.2)
@@ -277,7 +278,7 @@ GEM
gli (2.21.1) gli (2.21.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
gmail_xoauth (0.4.3) gmail_xoauth (0.4.2)
oauth (>= 0.3.6) oauth (>= 0.3.6)
google-apis-core (0.11.0) google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
@@ -316,16 +317,16 @@ GEM
google-cloud-translate-v3 (0.6.0) google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a) gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-protobuf (3.25.2) google-protobuf (3.22.3)
google-protobuf (3.25.2-arm64-darwin) google-protobuf (3.22.3-arm64-darwin)
google-protobuf (3.25.2-x86_64-darwin) google-protobuf (3.22.3-x86_64-darwin)
google-protobuf (3.25.2-x86_64-linux) google-protobuf (3.22.3-x86_64-linux)
googleapis-common-protos (1.4.0) googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2) googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27) grpc (~> 1.27)
googleapis-common-protos-types (1.11.0) googleapis-common-protos-types (1.6.0)
google-protobuf (~> 3.18) google-protobuf (~> 3.14)
googleauth (1.5.2) googleauth (1.5.2)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
@@ -335,13 +336,13 @@ GEM
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
groupdate (6.2.1) groupdate (6.2.1)
activesupport (>= 5.2) activesupport (>= 5.2)
grpc (1.54.3) grpc (1.54.0)
google-protobuf (~> 3.21) google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.54.3-x86_64-darwin) grpc (1.54.0-x86_64-darwin)
google-protobuf (~> 3.21) google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.54.3-x86_64-linux) grpc (1.54.0-x86_64-linux)
google-protobuf (~> 3.21) google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1) haikunator (1.1.1)
@@ -488,14 +489,14 @@ GEM
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.0) nio4r (2.7.0)
nokogiri (1.16.2) nokogiri (1.16.0)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.2-arm64-darwin) nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.2-x86_64-darwin) nokogiri (1.16.0-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux) nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
numo-narray (0.9.2.1) numo-narray (0.9.2.1)
oauth (1.1.0) oauth (1.1.0)
@@ -559,36 +560,36 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.3) racc (1.7.3)
rack (2.2.8.1) rack (2.2.8)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.4.0) rack-contrib (2.4.0)
rack (< 4) rack (< 4)
rack-cors (2.0.0) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.2.0) rack-mini-profiler (3.2.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (3.1.0) rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4) rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6) rack-proxy (0.7.7)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rack-timeout (0.6.3) rack-timeout (0.6.3)
rails (7.0.8.1) rails (7.0.8)
actioncable (= 7.0.8.1) actioncable (= 7.0.8)
actionmailbox (= 7.0.8.1) actionmailbox (= 7.0.8)
actionmailer (= 7.0.8.1) actionmailer (= 7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
actiontext (= 7.0.8.1) actiontext (= 7.0.8)
actionview (= 7.0.8.1) actionview (= 7.0.8)
activejob (= 7.0.8.1) activejob (= 7.0.8)
activemodel (= 7.0.8.1) activemodel (= 7.0.8)
activerecord (= 7.0.8.1) activerecord (= 7.0.8)
activestorage (= 7.0.8.1) activestorage (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.8.1) railties (= 7.0.8)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -596,9 +597,9 @@ GEM
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
railties (7.0.8.1) railties (7.0.8)
actionpack (= 7.0.8.1) actionpack (= 7.0.8)
activesupport (= 7.0.8.1) activesupport (= 7.0.8)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@@ -708,7 +709,6 @@ GEM
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.14.0) sentry-rails (5.14.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.14.0) sentry-ruby (~> 5.14.0)
@@ -794,7 +794,14 @@ GEM
valid_email2 (4.0.6) valid_email2 (4.0.6)
activemodel (>= 3.2) activemodel (>= 3.2)
mail (~> 2.5) mail (~> 2.5)
version_gem (1.1.3) 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)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -810,11 +817,6 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) 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) webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
@@ -918,10 +920,10 @@ DEPENDENCIES
puma puma
pundit pundit
rack-attack (>= 6.7.0) rack-attack (>= 6.7.0)
rack-cors (= 2.0.0) rack-cors
rack-mini-profiler (>= 3.2.0) rack-mini-profiler (>= 3.2.0)
rack-timeout rack-timeout
rails (~> 7.0.8.1) rails (~> 7.0.8.0)
redis redis
redis-namespace redis-namespace
responders (>= 3.1.1) responders (>= 3.1.1)
@@ -957,10 +959,10 @@ DEPENDENCIES
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2
vite_rails
web-console (>= 4.2.1) web-console (>= 4.2.1)
web-push web-push
webmock webmock
webpacker
wisper (= 2.0.0) wisper (= 2.0.0)
working_hours working_hours
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2017-2024 Chatwoot Inc. Copyright (c) 2017-2021 Chatwoot Inc.
Portions of this software are licensed as follows: Portions of this software are licensed as follows:
+1 -1
View File
@@ -1,4 +1,4 @@
backend: bin/rails s -p 3000 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 # https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
frontend: bin/vite dev
+1 -1
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050 backend: RAILS_ENV=test bin/rails s -p 5050
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server frontend: bin/vite
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
+1
View File
@@ -23,6 +23,7 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month"> <img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a> <a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a> <a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
-5
View File
@@ -72,11 +72,6 @@
"scripts": { "scripts": {
"test": "bundle exec rake test" "test": "bundle exec rake test"
} }
},
"review": {
"scripts": {
"postdeploy": "bundle exec rails db:seed"
}
} }
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ class ContactIdentifyAction
def existing_email_contact def existing_email_contact
return if params[:email].blank? return if params[:email].blank?
@existing_email_contact ||= account.contacts.from_email(params[:email]) @existing_email_contact ||= account.contacts.find_by(email: params[:email])
end end
def existing_phone_number_contact def existing_phone_number_contact
@@ -9,7 +9,7 @@
padding: 4px 12px; padding: 4px 12px;
.icon-container { .icon-container {
margin-right: 2px; margin-right: 4px;
} }
@@ -86,8 +86,5 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !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; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
+4 -14
View File
@@ -2,7 +2,7 @@
class AccountBuilder class AccountBuilder
include CustomExceptions::Account include CustomExceptions::Account
pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale] pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
def perform def perform
if @user.nil? if @user.nil?
@@ -15,22 +15,12 @@ class AccountBuilder
end end
[@user, @account] [@user, @account]
rescue StandardError => e rescue StandardError => e
Rails.logger.debug e.inspect puts e.inspect
raise e raise e
end end
private private
def user_full_name
# the empty string ensures that not-null constraint is not violated
@user_full_name || ''
end
def account_name
# the empty string ensures that not-null constraint is not violated
@account_name || ''
end
def validate_email def validate_email
address = ValidEmail2::Address.new(@email) address = ValidEmail2::Address.new(@email)
if address.valid? # && !address.disposable? if address.valid? # && !address.disposable?
@@ -49,7 +39,7 @@ class AccountBuilder
end end
def create_account def create_account
@account = Account.create!(name: account_name, locale: I18n.locale) @account = Account.create!(name: @account_name, locale: I18n.locale)
Current.account = @account Current.account = @account
end end
@@ -74,7 +64,7 @@ class AccountBuilder
@user = User.new(email: @email, @user = User.new(email: @email,
password: user_password, password: user_password,
password_confirmation: user_password, password_confirmation: user_password,
name: user_full_name) name: @user_full_name)
@user.type = 'SuperAdmin' if @super_admin @user.type = 'SuperAdmin' if @super_admin
@user.confirm if @confirmed @user.confirm if @confirmed
@user.save! @user.save!
-60
View File
@@ -1,60 +0,0 @@
# 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.from_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
@@ -55,8 +55,7 @@ class ContactInboxWithContactBuilder
email: contact_attributes[:email], email: contact_attributes[:email],
identifier: contact_attributes[:identifier], identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes], additional_attributes: contact_attributes[:additional_attributes],
custom_attributes: contact_attributes[:custom_attributes], custom_attributes: contact_attributes[:custom_attributes]
contact_type: contact_attributes[:type]
) )
end end
@@ -76,7 +75,7 @@ class ContactInboxWithContactBuilder
def find_contact_by_email(email) def find_contact_by_email(email)
return if email.blank? return if email.blank?
account.contacts.from_email(email) account.contacts.find_by(email: email.downcase)
end end
def find_contact_by_phone_number(phone_number) def find_contact_by_phone_number(phone_number)
-10
View File
@@ -54,13 +54,6 @@ class V2::ReportBuilder
} }
end end
def bot_summary
{
bot_resolutions_count: bot_resolutions.count,
bot_handoffs_count: bot_handoffs.count
}
end
def conversation_metrics def conversation_metrics
if params[:type].equal?(:account) if params[:type].equal?(:account)
live_conversations live_conversations
@@ -78,8 +71,6 @@ class V2::ReportBuilder
avg_first_response_time avg_first_response_time
avg_resolution_time reply_time avg_resolution_time reply_time
resolutions_count resolutions_count
bot_resolutions_count
bot_handoffs_count
reply_time].include?(params[:metric]) reply_time].include?(params[:metric])
end end
@@ -132,7 +123,6 @@ class V2::ReportBuilder
unattended: @open_conversations.unattended.count unattended: @open_conversations.unattended.count
} }
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account) metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
metric[:pending] = @open_conversations.pending.count if params[:type].equal?(:account)
metric metric
end end
end end
@@ -1,53 +0,0 @@
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
@@ -1,17 +0,0 @@
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
@@ -1,54 +0,0 @@
class V2::Reports::BotMetricsBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def metrics
{
conversation_count: bot_conversations.count,
message_count: bot_messages.count,
resolution_rate: bot_resolution_rate.to_i,
handoff_rate: bot_handoff_rate.to_i
}
end
private
def bot_activated_inbox_ids
@bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id)
end
def bot_conversations
@bot_conversations ||= account.conversations.where(inbox_id: bot_activated_inbox_ids).where(created_at: range)
end
def bot_messages
@bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range)
end
def bot_resolutions_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
created_at: range).distinct.count
end
def bot_handoffs_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct.count
end
def bot_resolution_rate
return 0 if bot_conversations.count.zero?
bot_resolutions_count.to_f / bot_conversations.count * 100
end
def bot_handoff_rate
return 0 if bot_conversations.count.zero?
bot_handoffs_count.to_f / bot_conversations.count * 100
end
end
@@ -1,49 +0,0 @@
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,26 +1,16 @@
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index, :bulk_create] before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization before_action :check_authorization
before_action :find_user, only: [:create]
before_action :validate_limit, only: [:create] before_action :validate_limit, only: [:create]
before_action :validate_limit_for_bulk_create, only: [:bulk_create] before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create]
def index def index
@agents = agents @agents = agents
end end
def create def create; end
builder = AgentBuilder.new(
email: new_agent_params['email'],
name: new_agent_params['name'],
role: new_agent_params['role'],
availability: new_agent_params['availability'],
auto_offline: new_agent_params['auto_offline'],
inviter: current_user,
account: Current.account
)
builder.perform
end
def update def update
@agent.update!(agent_params.slice(:name).compact) @agent.update!(agent_params.slice(:name).compact)
@@ -33,30 +23,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def bulk_create
emails = params[:emails]
emails.each do |email|
builder = AgentBuilder.new(
email: email,
name: email.split('@').first,
inviter: current_user,
account: Current.account
)
begin
builder.perform
rescue ActiveRecord::RecordInvalid => e
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
end
end
# 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 private
def check_authorization def check_authorization
@@ -67,34 +33,47 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
@agent = agents.find(params[:id]) @agent = agents.find(params[:id])
end end
def find_user
@user = User.find_by(email: new_agent_params[:email])
end
# TODO: move this to a builder and combine the save account user method into a builder
# ensure the account user association is also created in a single transaction
def create_user
return @user.send_confirmation_instructions if @user
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
end
def save_account_user
AccountUser.create!({
account_id: Current.account.id,
user_id: @user.id,
inviter_id: current_user.id
}.merge({
role: new_agent_params[:role],
availability: new_agent_params[:availability],
auto_offline: new_agent_params[:auto_offline]
}.compact))
end
def agent_params def agent_params
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline) params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
end end
def new_agent_params def new_agent_params
# intial string ensures the password requirements are met
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline) params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
end end
def agents def agents
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] }) @agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
end end
def validate_limit_for_bulk_create
limit_available = params[:emails].count <= available_agent_count
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
end
def validate_limit def validate_limit
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent? render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
end
def available_agent_count
Current.account.usage_limits[:agents] - agents.count
end
def can_add_agent?
available_agent_count.positive?
end end
def delete_user_record(agent) def delete_user_record(agent)
@@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def export def export
column_names = params['column_names'] column_names = params['column_names']
Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email) Account::ContactsExportJob.perform_later(Current.account.id, column_names)
head :ok, message: I18n.t('errors.contacts.export.success') head :ok, message: I18n.t('errors.contacts.export.success')
end end
@@ -148,7 +148,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {}) params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
end end
def contact_custom_attributes def contact_custom_attributes
@@ -36,10 +36,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
end end
def update
@conversation.update!(permitted_update_params)
end
def filter def filter
result = ::Conversations::FilterService.new(params.permit!, current_user).perform result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations] @conversations = result[:conversations]
@@ -114,11 +110,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private private
def permitted_update_params
# TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute
params.permit(:priority)
end
def update_last_seen_on_conversation(last_seen_at, update_assignee) def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations # rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at) @conversation.update_column(:agent_last_seen_at, last_seen_at)
@@ -185,5 +176,3 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.assignee_id? && Current.user == @conversation.assignee @conversation.assignee_id? && Current.user == @conversation.assignee
end end
end end
Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController')
@@ -2,13 +2,13 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
RESULTS_PER_PAGE = 15 RESULTS_PER_PAGE = 15
include DateRangeHelper include DateRangeHelper
before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread] before_action :fetch_notification, only: [:update, :destroy, :snooze]
before_action :set_primary_actor, only: [:read_all] before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@notifications = notification_finder.notifications
@unread_count = notification_finder.unread_count @unread_count = notification_finder.unread_count
@notifications = notification_finder.perform
@count = notification_finder.count @count = notification_finder.count
end end
@@ -29,33 +29,18 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification render json: @notification
end end
def unread
@notification.update(read_at: nil)
render json: @notification
end
def destroy def destroy
@notification.destroy @notification.destroy
head :ok head :ok
end end
def destroy_all
if params[:type] == 'read'
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
else
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
end
head :ok
end
def unread_count def unread_count
@unread_count = notification_finder.unread_count @unread_count = notification_finder.unread_count
render json: @unread_count render json: @unread_count
end end
def snooze def snooze
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil) @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
render json: @notification render json: @notification
end end
+3 -23
View File
@@ -5,13 +5,11 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception, skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
only: [:create], raise: false only: [:create], raise: false
before_action :check_signup_enabled, only: [:create] before_action :check_signup_enabled, only: [:create]
before_action :ensure_account_name, only: [:create]
before_action :validate_captcha, only: [:create] before_action :validate_captcha, only: [:create]
before_action :fetch_account, except: [:create] before_action :fetch_account, except: [:create]
before_action :check_authorization, except: [:create] before_action :check_authorization, except: [:create]
rescue_from CustomExceptions::Account::InvalidEmail, rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::InvalidParams,
CustomExceptions::Account::UserExists, CustomExceptions::Account::UserExists,
CustomExceptions::Account::UserErrors, CustomExceptions::Account::UserErrors,
with: :render_error_response with: :render_error_response
@@ -40,14 +38,11 @@ class Api::V1::AccountsController < Api::BaseController
def cache_keys def cache_keys
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
render json: { cache_keys: cache_keys_for_account }, status: :ok render json: { cache_keys: get_cache_keys }, status: :ok
end end
def update def update
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration)) @account.update!(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 end
def update_active_at def update_active_at
@@ -58,18 +53,7 @@ class Api::V1::AccountsController < Api::BaseController
private private
def ensure_account_name def get_cache_keys
# ensure that account_name and user_full_name is present
# this is becuase the account builder and the models validations are not triggered
# this change is to align the behaviour with the v2 accounts controller
# since these values are not required directly there
return if account_params[:account_name].present?
return if account_params[:user_full_name].present?
raise CustomExceptions::Account::InvalidParams.new({})
end
def cache_keys_for_account
{ {
label: fetch_value_for_key(params[:id], Label.name.underscore), label: fetch_value_for_key(params[:id], Label.name.underscore),
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore), inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
@@ -86,10 +70,6 @@ class Api::V1::AccountsController < Api::BaseController
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name) params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
end end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone)
end
def check_signup_enabled def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false' raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end end
+1 -12
View File
@@ -10,9 +10,7 @@ class Api::V1::ProfilesController < Api::BaseController
@user.update!(password_params.except(:current_password)) @user.update!(password_params.except(:current_password))
end end
@user.assign_attributes(profile_params) @user.update!(profile_params)
@user.custom_attributes.merge!(custom_attributes_params)
@user.save!
end end
def avatar def avatar
@@ -33,11 +31,6 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok head :ok
end end
def resend_confirmation
@user.send_confirmation_instructions unless @user.confirmed?
head :ok
end
private private
def set_user def set_user
@@ -64,10 +57,6 @@ class Api::V1::ProfilesController < Api::BaseController
) )
end end
def custom_attributes_params
params.require(:profile).permit(:phone_number)
end
def password_params def password_params
params.require(:profile).permit( params.require(:profile).permit(
:current_password, :current_password,
@@ -14,12 +14,6 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: summary_metrics render json: summary_metrics
end end
def bot_summary
summary = V2::ReportBuilder.new(Current.account, current_summary_params).bot_summary
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).bot_summary
render json: summary
end
def agents def agents
@report_data = generate_agents_report @report_data = generate_agents_report
generate_csv('agents_report', 'api/v2/accounts/reports/agents') generate_csv('agents_report', 'api/v2/accounts/reports/agents')
@@ -54,11 +48,6 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: conversation_metrics render json: conversation_metrics
end end
def bot_metrics
bot_metrics = V2::Reports::BotMetricsBuilder.new(Current.account, params).metrics
render json: bot_metrics
end
private private
def generate_csv(filename, template) def generate_csv(filename, template)
@@ -1,36 +0,0 @@
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
@@ -1,69 +0,0 @@
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,8 +1,7 @@
module AccessTokenAuthHelper module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = { BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update], 'api/v1/accounts/conversations' => %w[toggle_status create],
'api/v1/accounts/conversations/messages' => ['create'], 'api/v1/accounts/conversations/messages' => ['create']
'api/v1/accounts/conversations/assignments' => ['create']
}.freeze }.freeze
def ensure_access_token def ensure_access_token
@@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper
end end
def account_accessible_for_bot?(account) def account_accessible_for_bot?(account)
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end end
end end
+1 -1
View File
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
VAPID_PUBLIC_KEY: VapidService.public_key, VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?, IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''), AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH GIT_SHA: GIT_HASH
@@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
skip_before_action :authenticate_user!, raise: false skip_before_action :authenticate_user!, raise: false
def create def create
@user = User.from_email(params[:email]) @user = User.find_by(email: params[:email])
if @user if @user
@user.send_reset_password_instructions @user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200) build_response(I18n.t('messages.reset_password_success'), 200)
@@ -33,7 +33,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
def process_sso_auth_token def process_sso_auth_token
return if params[:email].blank? return if params[:email].blank?
user = User.from_email(params[:email]) user = User.find_by(email: params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token]) @resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end end
end end
@@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController
def show; end def show; end
def create def create
@resource = (User.from_email(user_params[:email]) || User.new(user_params)) @resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.skip_confirmation! @resource.skip_confirmation!
@resource.save! @resource.save!
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
@@ -1,31 +1,15 @@
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
include Events::Types include Events::Types
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
def index def index
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
end end
def show; end
def create def create
@conversation = create_conversation @conversation = create_conversation
end 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 def toggle_typing
case params[:typing_status] case params[:typing_status]
when 'on' when 'on'
@@ -46,11 +30,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
private private
def set_conversation def set_conversation
@conversation = if @contact_inbox.hmac_verified? @conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
@contact_inbox.contact.conversations.find_by!(display_id: params[:id])
else
@contact_inbox.conversations.find_by!(display_id: params[:id])
end
end end
def create_conversation def create_conversation
@@ -22,24 +22,19 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
i.value = value i.value = value
i.save! i.save!
end end
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully" # rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
# rubocop:enable Rails/I18nLocaleTexts
end end
private private
def set_config def set_config
@config = params[:config] || 'general' @config = params[:config]
end end
def allowed_configs def allowed_configs
@allowed_configs = case @config @allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
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
end end
@@ -5,10 +5,6 @@
# If you want to add pagination or other controller-level concerns, # If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions. # you're free to overwrite the RESTful controller actions.
class SuperAdmin::ApplicationController < Administrate::ApplicationController class SuperAdmin::ApplicationController < Administrate::ApplicationController
include ActionView::Helpers::TagHelper
include ActionView::Context
helper_method :render_vue_component
# authenticiation done via devise : SuperAdmin Model # authenticiation done via devise : SuperAdmin Model
before_action :authenticate_super_admin! before_action :authenticate_super_admin!
@@ -27,17 +23,6 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
private private
def render_vue_component(component_name, props = {})
html_options = {
id: 'app',
data: {
component_name: component_name,
props: props.to_json
}
}
content_tag(:div, '', html_options)
end
def invalid_action_perfomed def invalid_action_perfomed
# rubocop:disable Rails/I18nLocaleTexts # rubocop:disable Rails/I18nLocaleTexts
flash[:error] = 'Invalid action performed' flash[:error] = 'Invalid action performed'
@@ -28,7 +28,8 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end end
def sha def sha
@metrics['Git SHA'] = GIT_HASH sha = `git rev-parse HEAD`
@metrics['Git SHA'] = sha.presence || 'n/a'
end end
def postgres_status def postgres_status
+1 -7
View File
@@ -169,12 +169,6 @@ class ConversationFinder
) )
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
@conversations = @conversations.send(sort_by, sort_order) @conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
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
end end
+7 -16
View File
@@ -10,8 +10,8 @@ class NotificationFinder
set_up set_up
end end
def notifications def perform
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order) notifications
end end
def unread_count def unread_count
@@ -26,31 +26,22 @@ class NotificationFinder
def set_up def set_up
find_all_notifications find_all_notifications
filter_snoozed_notifications filter_by_status
fitler_read_notifications
end end
def find_all_notifications def find_all_notifications
@notifications = current_user.notifications.where(account_id: @current_account.id) @notifications = current_user.notifications.where(account_id: @current_account.id)
end end
def filter_snoozed_notifications def filter_by_status
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed') @notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == '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 end
def current_page def current_page
params[:page] || 1 params[:page] || 1
end end
def sort_order def notifications
params[:sort_order] || :desc @notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
end end
end end
+10 -10
View File
@@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper
dates = data.pluck(:date).uniq.sort dates = data.pluck(:date).uniq.sort
# add the dates as the first row, leave an empty cell for the hour column # add the dates as the first row, leave an empty cell for the hour column
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'] # e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
result_arr << (['Start of the hour'] + dates) result_arr << ([nil] + dates)
# group the data by hour, we do not need to sort it, because the data is already sorted # group the data by hour, we do not need to sort it, because the data is already sorted
# given it starts from the beginning of the day # given it starts from the beginning of the day
@@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...] # value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
data.group_by { |d| d[:hour] }.each do |hour, items| data.group_by { |d| d[:hour] }.each do |hour, items|
# create a new row for each hour # create a new row for each hour
row = [format('%02d:00', hour)] row = [hour]
# group the items by date, so we can easily access the value for each date # group the items by date, so we can easily access the value for each date
# grouped values will be a hasg with the date as the key, and the value as the value # grouped values will be a hasg with the date as the key, and the value as the value
@@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array)) row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
end end
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4] # row will look like [22, 0, 0, 1, 4, 6, 7, 4]
# add the row to the result array # add the row to the result array
result_arr << row result_arr << row
@@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper
# return the resultant array # return the resultant array
# the result looks like this # the result looks like this
# [ # [
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'], # [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
# ['00:00', 0, 0, 0], # [0, 0, 0, 0],
# ['01:00', 0, 0, 0], # [1, 0, 0, 0],
# ['02:00', 0, 0, 0], # [2, 0, 0, 0],
# ['03:00', 0, 0, 0], # [3, 0, 0, 0],
# ['04:00', 0, 0, 0], # [4, 0, 0, 0],
# ] # ]
result_arr result_arr
end end
@@ -45,8 +45,12 @@ module Api::V2::Accounts::ReportsHelper
def generate_readable_report_metrics(report_metric) def generate_readable_report_metrics(report_metric)
[ [
report_metric[:conversations_count], report_metric[:conversations_count],
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format, time_to_minutes(report_metric[:avg_first_response_time]),
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format time_to_minutes(report_metric[:avg_resolution_time])
] ]
end end
def time_to_minutes(time_in_seconds)
(time_in_seconds / 60).to_i
end
end end
-83
View File
@@ -1,83 +0,0 @@
module ContactHelper
def parse_name(full_name)
# If the input is nil or not a string, return a hash with all values set to nil
return default_name_hash if invalid_name?(full_name)
# If the input is a number, return a hash with the number as the first name
return numeric_name_hash(full_name) if valid_number?(full_name)
full_name = full_name.squish
# If full name consists of only one word, consider it as the first name
return single_word_name_hash(full_name) if single_word?(full_name)
parts = split_name(full_name)
parts = handle_conjunction(parts)
build_name_hash(parts)
end
private
def default_name_hash
{ first_name: nil, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def invalid_name?(full_name)
!full_name.is_a?(String) || full_name.empty?
end
def numeric_name_hash(full_name)
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def valid_number?(full_name)
full_name.gsub(/\s+/, '').match?(/\A\+?\d+\z/)
end
def single_word_name_hash(full_name)
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def single_word?(full_name)
full_name.split.size == 1
end
def split_name(full_name)
full_name.split
end
def handle_conjunction(parts)
conjunctions = ['and', '&']
parts.each_index do |i|
next unless conjunctions.include?(parts[i]) && i.positive?
parts[i - 1] = [parts[i - 1], parts[i + 1]].join(' ')
parts.delete_at(i)
parts.delete_at(i)
end
parts
end
def build_name_hash(parts)
suffix = parts.pop if parts.last.match?(/(\w+\.|[IVXLM]+|[A-Z]+)$/)
last_name = parts.pop
prefix = parts.shift if parts.first.match?(/^\w+\./)
first_name = parts.shift
middle_name = parts.join(' ')
hash = {
first_name: first_name,
last_name: last_name,
prefix: prefix,
middle_name: middle_name,
suffix: suffix
}
# Reverse name if "," was used in Last, First notation.
if hash[:first_name] =~ /,$/
hash[:first_name] = hash[:last_name]
hash[:last_name] = Regexp.last_match.pre_match
end
hash
end
end
-18
View File
@@ -32,14 +32,6 @@ module ReportHelper
(get_grouped_values resolutions).count (get_grouped_values resolutions).count
end end
def bot_resolutions_count
(get_grouped_values bot_resolutions).count
end
def bot_handoffs_count
(get_grouped_values bot_handoffs).count
end
def conversations def conversations
scope.conversations.where(account_id: account.id, created_at: range) scope.conversations.where(account_id: account.id, created_at: range)
end end
@@ -57,16 +49,6 @@ module ReportHelper
conversations: { status: :resolved }, created_at: range).distinct conversations: { status: :resolved }, created_at: range).distinct
end end
def bot_resolutions
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
conversations: { status: :resolved }, created_at: range).distinct
end
def bot_handoffs
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct
end
def avg_first_response_time def avg_first_response_time
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id)) grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id))
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
@@ -1,9 +0,0 @@
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 -9
View File
@@ -2,14 +2,13 @@
<div <div
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app" id="app"
class="flex-grow-0 w-full h-full min-h-0 app-wrapper" class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
:class="{ 'app-rtl--wrapper': isRTLView }" :class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'" :dir="isRTLView ? 'rtl' : 'ltr'"
> >
<update-banner :latest-chatwoot-version="latestChatwootVersion" /> <update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="currentAccountId"> <template v-if="currentAccountId">
<pending-email-verification-banner v-if="hideOnOnboardingView" /> <payment-pending-banner />
<payment-pending-banner v-if="hideOnOnboardingView" />
<upgrade-banner /> <upgrade-banner />
</template> </template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@@ -33,12 +32,10 @@ import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue'; import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue'; import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable'; import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer.vue'; import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper'; import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { import {
registerSubscription, registerSubscription,
verifyServiceWorkerExistence, verifyServiceWorkerExistence,
@@ -55,7 +52,6 @@ export default {
PaymentPendingBanner, PaymentPendingBanner,
WootSnackbarBox, WootSnackbarBox,
UpgradeBanner, UpgradeBanner,
PendingEmailVerificationBanner,
}, },
mixins: [rtlMixin], mixins: [rtlMixin],
@@ -80,9 +76,6 @@ export default {
const { accounts = [] } = this.currentUser || {}; const { accounts = [] } = this.currentUser || {};
return accounts.length > 0; return accounts.length > 0;
}, },
hideOnOnboardingView() {
return !isOnOnboardingView(this.$route);
},
}, },
watch: { watch: {
-8
View File
@@ -1,17 +1,9 @@
/* global axios */
import ApiClient from './ApiClient'; import ApiClient from './ApiClient';
class Agents extends ApiClient { class Agents extends ApiClient {
constructor() { constructor() {
super('agents', { accountScoped: true }); super('agents', { accountScoped: true });
} }
bulkInvite({ emails }) {
return axios.post(`${this.url}/bulk_create`, {
emails,
});
}
} }
export default new Agents(); export default new Agents();
+2 -7
View File
@@ -29,12 +29,11 @@ export default {
return fetchPromise; return fetchPromise;
}, },
hasAuthCookie() { hasAuthCookie() {
return !!Cookies.get('cw_d_session_info'); return !!Cookies.getJSON('cw_d_session_info');
}, },
getAuthData() { getAuthData() {
if (this.hasAuthCookie()) { if (this.hasAuthCookie()) {
const savedAuthInfo = Cookies.get('cw_d_session_info'); return Cookies.getJSON('cw_d_session_info');
return JSON.parse(savedAuthInfo || '{}');
} }
return false; return false;
}, },
@@ -98,8 +97,4 @@ export default {
}, },
}); });
}, },
resendConfirmation() {
const urlData = endPoints('resendConfirmation');
return axios.post(urlData.url);
},
}; };
@@ -47,10 +47,6 @@ const endPoints = {
setActiveAccount: { setActiveAccount: {
url: '/api/v1/profile/set_active_account', url: '/api/v1/profile/set_active_account',
}, },
resendConfirmation: {
url: '/api/v1/profile/resend_confirmation',
},
}; };
export default page => { export default page => {
+2 -30
View File
@@ -6,16 +6,8 @@ class NotificationsAPI extends ApiClient {
super('notifications', { accountScoped: true }); super('notifications', { accountScoped: true });
} }
get({ page, status, type, sortOrder }) { get(page) {
const includesFilter = [status, type].filter(value => !!value); return axios.get(`${this.url}?page=${page}`);
return axios.get(this.url, {
params: {
page,
sort_order: sortOrder,
includes: includesFilter,
},
});
} }
getNotifications(contactId) { getNotifications(contactId) {
@@ -33,29 +25,9 @@ class NotificationsAPI extends ApiClient {
}); });
} }
unRead(id) {
return axios.post(`${this.url}/${id}/unread`);
}
readAll() { readAll() {
return axios.post(`${this.url}/read_all`); return axios.post(`${this.url}/read_all`);
} }
delete(id) {
return axios.delete(`${this.url}/${id}`);
}
deleteAll({ type = 'all' }) {
return axios.post(`${this.url}/destroy_all`, {
type,
});
}
snooze({ id, snoozedUntil = null }) {
return axios.post(`${this.url}/${id}/snooze`, {
snoozed_until: snoozedUntil,
});
}
} }
export default new NotificationsAPI(); export default new NotificationsAPI();
-18
View File
@@ -84,24 +84,6 @@ class ReportsAPI extends ApiClient {
params: { since, until, business_hours: businessHours }, params: { since, until, business_hours: businessHours },
}); });
} }
getBotMetrics({ from, to } = {}) {
return axios.get(`${this.url}/bot_metrics`, {
params: { since: from, until: to },
});
}
getBotSummary({ from, to, groupBy, businessHours } = {}) {
return axios.get(`${this.url}/bot_summary`, {
params: {
since: from,
until: to,
type: 'account',
group_by: groupBy,
business_hours: businessHours,
},
});
}
} }
export default new ReportsAPI(); export default new ReportsAPI();
-9
View File
@@ -1,9 +0,0 @@
import ApiClient from './ApiClient';
class SlaAPI extends ApiClient {
constructor() {
super('sla_policies', { accountScoped: true });
}
}
export default new SlaAPI();
@@ -10,29 +10,4 @@ describe('#AgentAPI', () => {
expect(agents).toHaveProperty('update'); expect(agents).toHaveProperty('update');
expect(agents).toHaveProperty('delete'); 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,37 +27,11 @@ describe('#NotificationAPI', () => {
window.axios = originalAxios; window.axios = originalAxios;
}); });
describe('#get', () => { it('#get', () => {
it('generates the API call if both params are available', () => { notificationsAPI.get(1);
notificationsAPI.get({ expect(axiosMock.get).toHaveBeenCalledWith(
page: 1, '/api/v1/notifications?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', () => { it('#getNotifications', () => {
@@ -91,30 +65,5 @@ describe('#NotificationAPI', () => {
'/api/v1/notifications/read_all' '/api/v1/notifications/read_all'
); );
}); });
it('#snooze', () => {
notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 });
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/1/snooze',
{
snoozed_until: 12332211,
}
);
});
it('#delete', () => {
notificationsAPI.delete(1);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1');
});
it('#deleteAll', () => {
notificationsAPI.deleteAll({ type: 'all' });
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/destroy_all',
{
type: 'all',
}
);
});
}); });
}); });
@@ -111,40 +111,6 @@ describe('#Reports API', () => {
}); });
}); });
it('#getBotMetrics', () => {
reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 });
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/bot_metrics',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getBotSummary', () => {
reportsAPI.getBotSummary({
from: 1621103400,
to: 1621621800,
groupBy: 'date',
businessHours: true,
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/bot_summary',
{
params: {
since: 1621103400,
until: 1621621800,
type: 'account',
group_by: 'date',
business_hours: true,
},
}
);
});
it('#getConversationMetric', () => { it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account'); reportsAPI.getConversationMetric('account');
expect(axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
@@ -1,4 +1,4 @@
@import '~vue2-datepicker/scss/index'; @import 'vue2-datepicker/scss/index';
.date-picker { .date-picker {
&.no-margin { &.no-margin {
@@ -1,4 +1,4 @@
@import '~dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
.formulate-input { .formulate-input {
.formulate-input-errors { .formulate-input-errors {
@@ -0,0 +1,58 @@
.button {
font-family: $body-font-family;
font-weight: $font-weight-medium;
&.round {
border-radius: 1000px;
}
}
select {
height: 2.5rem;
}
.card {
margin-bottom: var(--space-small);
padding: var(--space-normal);
}
code {
border: 0;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
font-size: $font-size-mini;
&.hljs {
background: $color-background;
border-radius: var(--border-radius-large);
padding: $space-two;
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
}
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-capitalize {
text-transform: capitalize;
}
.cursor-pointer {
cursor: pointer;
}
// remove when grid gutters are fixed
.columns.with-right-space {
padding-right: var(--space-normal);
}
.badge {
border-radius: var(--border-radius-normal);
}
.padding-right-small {
padding-right: var(--space-one);
}
@@ -1,22 +1,59 @@
// loader class .bg-light {
@apply bg-slate-25 dark:bg-slate-800;
}
.bottom-space-fix {
margin-bottom: auto;
}
.full-height {
@include full-height();
}
.spinner { .spinner {
@include color-spinner(); @include color-spinner();
@apply inline-block h-6 py-0 px-6 relative align-middle w-6; display: inline-block;
height: $space-medium;
padding: $zero $space-medium;
position: relative;
vertical-align: middle;
width: $space-medium;
&.message { &.message {
@include normal-shadow; @include normal-shadow;
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0; background: $color-white;
border-radius: $space-large;
left: 0;
margin: $space-slab auto;
padding: $space-normal;
top: 0;
&::before { &::before {
@apply -ml-3 -mt-3; margin-left: -$space-slab;
margin-top: -$space-slab;
} }
} }
&.small { &.small {
@apply h-4 w-4; height: $space-normal;
width: $space-normal;
&::before { &::before {
@apply h-4 -mt-2 w-4; height: $space-normal;
margin-top: -$space-small;
width: $space-normal;
} }
} }
} }
.justify-space-between {
justify-content: space-between;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
@@ -1,19 +1,5 @@
// scss-lint:disable SpaceAfterPropertyColon
// @import 'shared/assets/fonts/inter';
html, html,
body { body {
font-family:
'PlusJakarta',
Inter,
-apple-system,
system-ui,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif !important;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
height: 100%; height: 100%;
@@ -1,5 +1,5 @@
@import '~dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
@import '~widget/assets/scss/mixins'; @import 'widget/assets/scss/mixins';
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
@@ -131,12 +131,37 @@
} }
} }
.search-header--wrap {
.search--input {
text-align: right;
}
.layout-switch__container {
transform: rotate(180deg);
}
}
// Basic filter dropdown // Basic filter dropdown
.basic-filter { .basic-filter {
left: 0; left: 0;
right: unset; right: unset;
} }
// Card label
.label-container {
.label {
margin-left: var(--space-smaller);
margin-right: 0;
}
}
// Secondary sidebar toggle button
.toggle-sidebar {
margin-left: 0;
margin-right: var(--space-minus-small);
transform: rotate(180deg);
}
// Bulk actions // Bulk actions
.bulk-action__container { .bulk-action__container {
.triangle { .triangle {
@@ -177,6 +202,22 @@
} }
} }
// Notification panel
.notification-wrap {
left: 0;
right: var(--space-jumbo);
.action-button {
margin-left: var(--space-small);
margin-right: 0;
}
.notification-content--wrap {
margin-left: 0;
margin-right: var(--space-small);
}
}
// Help center // Help center
.article-container .row--article-block { .article-container .row--article-block {
td:last-child { td:last-child {
@@ -283,6 +324,10 @@
// Other changes // Other changes
.account-selector--wrap {
direction: initial;
}
.colorpicker--chrome { .colorpicker--chrome {
direction: initial; direction: initial;
} }
@@ -302,4 +347,9 @@
.contact--form .input-group { .contact--form .input-group {
direction: initial; direction: initial;
} }
// scss-lint:disable QualifyingElement
.dropdown-menu--header > span.title {
text-align: right;
}
} }
@@ -0,0 +1,33 @@
.page-title {
font-size: $font-size-big;
}
.page-sub-title {
font-size: $font-size-large;
word-wrap: break-word;
}
.block-title {
font-size: $font-size-medium;
}
.sub-block-title {
font-size: $font-size-default;
}
.text-block-title {
font-size: $font-size-small;
}
.text-muted {
color: var(--s-300);
}
a {
font-size: $font-size-small;
}
p {
font-size: $font-size-small;
word-spacing: .12em;
}
@@ -0,0 +1,73 @@
.margin-bottom-small {
margin-bottom: var(--space-small);
}
.margin-right-smaller {
margin-right: var(--space-smaller);
}
.margin-left-minus-slab {
margin-left: var(--space-minus-slab);
}
.margin-right-minus-slab {
margin-right: var(--space-minus-slab);
}
.fs-small {
font-size: var(--font-size-small);
}
.fs-default {
font-size: var(--font-size-default);
}
.fw-medium {
font-weight: var(--font-weight-medium);
}
.p-normal {
padding: var(--space-normal);
}
.overflow-scroll {
overflow: scroll;
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
.border-right {
@apply border-r border-slate-50 dark:border-slate-700;
}
.border-left {
border-left: 1px solid var(--color-border);
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-between {
align-items: center;
display: flex;
justify-content: space-between;
}
.flex-end {
display: flex;
justify-content: end;
}
.flex-align-center {
align-items: center;
display: flex;
}
@@ -89,9 +89,6 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !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; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
@@ -19,24 +19,31 @@
@import 'formulate'; @import 'formulate';
@import 'date-picker'; @import 'date-picker';
@import 'typography';
@import 'layout'; @import 'layout';
@import 'animations'; @import 'animations';
@import 'rtl'; @import 'rtl';
@import 'widgets/base';
@import 'widgets/buttons'; @import 'widgets/buttons';
@import 'widgets/conv-header';
@import 'widgets/conversation-card';
@import 'widgets/conversation-view'; @import 'widgets/conversation-view';
@import 'widgets/forms';
@import 'widgets/modal';
@import 'widgets/reply-box';
@import 'widgets/report';
@import 'widgets/snackbar';
@import 'widgets/states';
@import 'widgets/tabs'; @import 'widgets/tabs';
@import 'widgets/woot-tables'; @import 'widgets/woot-tables';
@import 'views/settings/inbox';
@import 'views/settings/integrations';
@import 'plugins/multiselect'; @import 'plugins/multiselect';
@import 'plugins/dropdown'; @import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons'; @import 'utility-helpers';
.tooltip { .tooltip {
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900; @apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
} }
.hide {
@apply hidden;
}
@@ -1,7 +1,46 @@
.dropdown-pane { .dropdown-pane {
@apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999]; @include elegant-card;
@include border-light;
box-sizing: content-box;
padding: var(--space-small);
width: fit-content;
z-index: var(--z-index-very-high);
&.dropdown-pane--open { &.dropdown-pane--open {
@apply bg-white absolute dark:bg-slate-800 block visible; @apply bg-white dark:bg-slate-800;
display: block;
visibility: visible;
}
&.dropdowm--bottom {
&::before {
@include arrow(top, var(--color-border-light), 14px);
position: absolute;
right: 6px;
top: -14px;
}
&::after {
@include arrow(top, $color-white, var(--space-slab));
position: absolute;
right: var(--space-small);
top: -12px;
}
}
&.dropdowm--top {
&::before {
@include arrow(bottom, var(--color-border-light), 14px);
bottom: -14px;
position: absolute;
right: 6px;
}
&::after {
@include arrow(bottom, $color-white, var(--space-slab));
bottom: -12px;
position: absolute;
right: var(--space-small);
}
} }
} }
@@ -97,10 +97,6 @@
.multiselect__tags { .multiselect__tags {
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0; @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0;
input {
@apply border-0 border-none;
}
} }
.multiselect__tags-wrap { .multiselect__tags-wrap {
@@ -153,6 +149,7 @@
} }
.multiselect-wrap--small { .multiselect-wrap--small {
.multiselect__tags, .multiselect__tags,
.multiselect__input, .multiselect__input,
.multiselect { .multiselect {
@@ -183,6 +180,7 @@
.multiselect--disabled .multiselect__select { .multiselect--disabled .multiselect__select {
@apply bg-transparent; @apply bg-transparent;
} }
} }
.multiselect-wrap--medium { .multiselect-wrap--medium {
@@ -10,7 +10,6 @@
@import 'variables'; @import 'variables';
@import 'vue-multiselect/dist/vue-multiselect.min.css'; @import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins'; @import 'mixins';
@import 'helper-classes'; @import 'helper-classes';
@@ -20,7 +19,7 @@
@import 'animations'; @import 'animations';
@import 'widgets/buttons'; @import 'widgets/buttons';
@import 'widgets/base'; @import 'widgets/forms';
@import 'plugins/multiselect'; @import 'plugins/multiselect';
@@ -30,6 +29,7 @@
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
@import 'widget/assets/scss/utilities'; @import 'widget/assets/scss/utilities';
html, html,
body { body {
font-family: 'PlusJakarta', sans-serif; font-family: 'PlusJakarta', sans-serif;
@@ -1 +1,112 @@
// to be removed .settings {
@apply overflow-auto;
}
.wizard-box {
.item {
@apply cursor-pointer py-4 pr-4 pl-6 relative;
&::before,
&::after {
@apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5;
}
&::before {
@apply h-4 top-0;
}
&:first-child {
&::before {
@apply h-0;
}
}
&:last-child {
&::after {
@apply h-0;
}
}
&.active {
h3 {
@apply text-woot-500 dark:text-woot-500;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
}
}
&.over {
&::after {
@apply bg-woot-500 dark:bg-woot-500;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
}
& + .item {
&::before {
@apply bg-woot-500 dark:bg-woot-500;
}
}
}
h3 {
@apply text-slate-800 dark:text-slate-100 text-base pl-6;
}
.completed {
@apply text-green-500 dark:text-green-500 ml-1;
}
p {
@apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6;
}
.step {
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5;
i {
@apply text-xxs;
}
}
}
}
.wizard-body {
@apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6;
&.height-auto {
@apply h-auto;
}
}
.settings--content {
@apply my-2 mx-8;
.title {
@apply font-medium;
}
.code {
@apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap;
code {
@apply bg-transparent border-0;
}
}
}
.login-init {
@apply pt-[30%] text-center;
p {
@apply p-6;
}
> a > img {
@apply w-60;
}
}
@@ -1,146 +0,0 @@
// scss-lint:disable QualifyingElement
// Base typography
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-medium text-slate-800 dark:text-slate-50;
}
p {
text-rendering: optimizeLegibility;
word-spacing: 0.12em;
@apply mb-2 leading-[1.65] text-sm;
a {
@apply text-woot-500 dark:text-woot-500 cursor-pointer;
}
}
a {
@apply text-sm;
}
hr {
@apply clear-both max-w-full h-0 my-5 mx-0 border-slate-300 dark:border-slate-600;
}
ul,
ol,
dl {
@apply mb-2 list-disc list-outside leading-[1.65];
}
// Form elements
label {
@apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium;
&.error {
input {
@apply mb-1;
}
}
}
.input-wrap,
.help-text {
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
.help-text {
@apply font-normal text-slate-600 dark:text-slate-400;
}
}
// Focus outline removal
.button,
textarea,
input:focus {
outline: none;
}
// Inputs
input[type='text'],
input[type='number'],
input[type='password'],
input[type='date'],
input[type='email'],
input[type='url'] {
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
}
}
input[type='file'] {
@apply bg-white dark:bg-slate-800 leading-[1.15] mb-4;
}
// Select
select {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
background-position: right -1rem center;
background-size: 9px 6px;
@apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
}
// Textarea
textarea {
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
}
}
// Error handling
.has-multi-select-error {
div.multiselect {
@apply mb-1;
}
}
.error {
input,
input:not([type]),
textarea,
select,
.multiselect > .multiselect__tags,
.multiselect:not(.no-margin) {
@apply border border-solid border-red-400 dark:border-red-400 mb-1;
}
.message {
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
}
}
.input-group.small {
input {
@apply text-sm h-8;
}
.error {
@apply border-red-400 dark:border-red-400;
}
}
// Code styling
code {
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
@apply text-xs border-0;
&.hljs {
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5;
.hljs-number,
.hljs-string {
@apply text-red-800 dark:text-red-400;
}
}
}
@@ -1,40 +1,8 @@
// scss-lint:disable SpaceAfterPropertyColon
// scss-lint:disable MergeableSelector
button {
font-family: inherit;
transition:
background-color 0.25s ease-out,
color 0.25s ease-out;
@apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem];
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
}
.button-group {
@apply mb-0 flex flex-nowrap items-stretch;
.button {
flex: 0 0 auto;
@apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse;
}
.button--only-icon {
@apply w-10 justify-center pl-0 pr-0;
}
}
.back-button {
@apply m-0;
}
.button { .button {
@apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium; @apply items-center inline-flex h-10 mb-0 gap-2;
.button__content { .button__content {
@apply w-full whitespace-nowrap overflow-hidden text-ellipsis; @apply w-full;
img, img,
svg { svg {
@@ -42,61 +10,12 @@ button {
} }
} }
&:hover {
@apply bg-woot-600 dark:bg-woot-600;
}
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
&.success {
@apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white;
}
&.secondary {
@apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white;
}
&.primary {
@apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white;
}
&.clear {
@apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent;
}
&.alert {
@apply bg-red-500 dark:bg-red-500 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.warning {
@apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.tiny {
@apply h-6 text-[10px];
}
&.small {
@apply h-8 text-xs;
}
.spinner { .spinner {
@apply px-2 py-0; @apply px-2 py-0;
} }
// @TODDO - Remove after moving all buttons to woot-button // @TODDO - Remove after moving all buttons to woot-button
.icon+.button__content { .icon + .button__content {
@apply w-auto; @apply w-auto;
} }
@@ -115,7 +34,7 @@ button {
} }
&.hollow { &.hollow {
@apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; @apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
&.secondary { &.secondary {
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
@@ -0,0 +1 @@
// File to be removed
@@ -0,0 +1,16 @@
@keyframes left-shift-animation {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(1px);
}
}
.conversation {
&.active {
animation: left-shift-animation 0.25s $swift-ease-out-function;
}
}
@@ -79,7 +79,7 @@
@apply rounded-r-lg rounded-l mr-auto break-words; @apply rounded-r-lg rounded-l mr-auto break-words;
&:not(.is-unsupported) { &:not(.is-unsupported) {
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50; @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
} }
&.is-image { &.is-image {
@@ -91,7 +91,7 @@
} }
.file { .file {
.attachment-name { .text-block-title {
@apply text-slate-700 dark:text-woot-300; @apply text-slate-700 dark:text-woot-300;
} }
@@ -222,6 +222,20 @@
@apply flex relative flex-col; @apply flex relative flex-col;
} }
.typing-indicator-wrap {
@apply items-center flex h-0 absolute w-full -top-8;
.typing-indicator {
@include elegant-card;
@include round-corner;
@apply py-2 pr-4 pl-5 bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100 text-xs font-semibold my-2.5 mx-auto;
.gif {
@apply ml-2 w-6;
}
}
}
.left .bubble .text-content { .left .bubble .text-content {
h1, h1,
h2, h2,
@@ -0,0 +1,78 @@
// scss-lint:disable QualifyingElement
label {
@apply text-slate-800 dark:text-slate-200;
}
textarea {
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
}
input {
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600;
}
}
input[type='file'] {
@apply bg-white dark:bg-slate-800;
}
select {
@apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
}
.error {
input[type='color'],
input[type='date'],
input[type='datetime'],
input[type='datetime-local'],
input[type='email'],
input[type='month'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='time'],
input[type='url'],
input[type='week'],
input:not([type]),
textarea,
select,
.multiselect > .multiselect__tags {
@apply border border-solid border-red-400 dark:border-red-400;
}
.message {
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
}
}
.button,
textarea,
input {
&:focus {
outline: none;
}
}
.input-wrap {
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
}
.help-text {
@apply font-normal text-slate-600 dark:text-slate-400;
}
.input-group.small {
input {
@apply text-sm h-8;
}
.error {
@apply border-red-400 dark:border-red-400;
}
}
@@ -0,0 +1,71 @@
.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;
}
.page-top-bar {
@apply px-8 pt-9 pb-0;
img {
@apply max-h-[3.75rem];
}
}
.modal-container {
@apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem];
&.medium {
@apply max-w-[80%] w-[56.25rem];
}
.content-box {
@apply h-auto p-0;
}
h2 {
@apply text-slate-800 dark:text-slate-100 text-lg font-semibold;
}
p {
@apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300;
}
.content {
@apply p-8;
}
form,
.modal-content {
@apply pt-4 pb-8 px-8 self-center;
a {
@apply p-4;
}
}
.modal-footer {
@apply flex justify-end items-center py-2 px-0 gap-2;
&.justify-content-end {
@apply justify-end;
}
}
.delete-item {
@apply p-8;
button {
@apply m-0;
}
}
}
.modal-enter,
.modal-leave {
@apply opacity-0;
}
.modal-enter .modal-container,
.modal-leave .modal-container {
transform: scale(1.1);
// @apply transform scale-110;
}
@@ -0,0 +1,60 @@
.reply-box {
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: var(--slate-500);
cursor: pointer;
font-size: $font-size-medium;
margin-right: $space-small;
&.active {
color: $color-woot;
}
}
.attachment {
cursor: pointer;
margin-right: $space-one;
padding: 0 $space-small;
}
.video-js {
background: transparent;
// Override min-height : 50px in foundation
//
max-height: $space-mega * 2.4;
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
>textarea {
@include ghost-input();
background: transparent;
margin: 0;
max-height: $space-mega * 2.4;
// Override min-height : 50px in foundation
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
}
&.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800;
>input {
@apply bg-yellow-100 dark:bg-yellow-800;
}
}
}
}
@@ -0,0 +1,60 @@
.report-card {
@include custom-border-top(3px, transparent);
cursor: pointer;
margin: 0;
padding: var(--space-normal);
&.active {
@include custom-border-top(3px, var(--color-woot));
@include background-white;
.heading,
.metric {
color: var(--color-woot);
}
}
.heading {
align-items: center;
color: var(--color-heading);
display: flex;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin: 0;
}
.info-icon {
color: var(--b-400);
margin-left: var(--space-micro);
}
.metric-wrap {
align-items: center;
display: flex;
}
.metric {
font-size: var(--font-size-big);
font-weight: var(--font-weight-feather);
margin-top: var(--space-smaller);
}
.metric-trend {
font-size: var(--font-size-small);
margin: 0 var(--space-small);
}
.metric-up {
color: $success-color;
}
.metric-down {
color: $alert-color;
}
.desc {
font-size: var(--font-size-small);
margin: 0;
text-transform: capitalize;
}
}
@@ -0,0 +1,29 @@
.reports-option__rounded--item {
border-radius: 100%;
height: var(--space-two);
width: var(--space-two);
}
.reports-option__item {
flex-shrink: 0;
margin-right: var(--space-small);
}
.reports-option__label--swatch {
border: 1px solid var(--color-border);
}
.reports-option__wrap {
align-items: center;
display: flex;
}
.reports-option__title {
margin: 0 var(--space-small);
}
.switch {
margin-bottom: var(--space-zero);
margin-left: var(--space-small);
}
@@ -0,0 +1,18 @@
.search {
@include flex;
@include flex-align($x: left, $y: middle);
@include flex-shrink;
padding: $space-one $space-normal;
transition: all 0.3s var(--ease-in-out-quad);
> .icon {
color: $medium-gray;
font-size: $font-size-medium;
}
> input {
@include ghost-input();
margin: 0;
}
}
@@ -0,0 +1,43 @@
// 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);
position: relative;
&:hover {
background: var(--color-background-light);
}
.dropdown-pane {
bottom: 3.75rem;
display: block;
visibility: visible;
width: fit-content;
}
.active {
border-bottom: 2px solid $medium-gray;
}
}
.hamburger--menu {
cursor: pointer;
display: block;
margin-right: var(--space-normal);
}
.header--icon {
display: block;
margin: 0 var(--space-small) 0 var(--space-smaller);
@media screen and (max-width: 1200px) {
display: none;
}
}
.header-title {
margin: 0 var(--space-small);
}
@@ -0,0 +1,45 @@
.ui-snackbar-container {
left: 0;
margin: 0 auto;
max-width: 25rem;
overflow: hidden;
position: absolute;
right: 0;
text-align: center;
top: $space-normal;
z-index: 9999;
}
.ui-snackbar {
@include shadow;
background-color: $woot-snackbar-bg;
border-radius: $space-smaller;
display: inline-flex;
margin-bottom: $space-small;
max-width: 25rem;
min-height: 1.875rem;
min-width: 15rem;
padding: $space-slab $space-medium;
text-align: left;
}
.ui-snackbar-text {
color: $color-white;
font-size: $font-size-small;
font-weight: $font-weight-medium;
}
.ui-snackbar-action {
margin-left: auto;
padding-left: 1.875rem;
button {
background: none;
border: 0;
color: $woot-snackbar-button;
font-size: $font-size-small;
margin: 0;
padding: 0;
text-transform: uppercase;
}
}
@@ -0,0 +1 @@
// To be removed

Some files were not shown because too many files have changed in this diff Show More