Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2b75ca17 | ||
|
|
71ee10c889 | ||
|
|
978a8a4cb2 | ||
|
|
cd06b2b337 | ||
|
|
6eb06377cc | ||
|
|
94892e7168 | ||
|
|
fe744abe26 | ||
|
|
a902b49bc5 | ||
|
|
9c07b6dd46 | ||
|
|
3b7b06dbec | ||
|
|
597a4164e8 | ||
|
|
678c00f254 | ||
|
|
7298002da7 | ||
|
|
96b781b7fd | ||
|
|
1ce5cbe275 | ||
|
|
c607f09be0 | ||
|
|
33a6ad9f7e | ||
|
|
e662336eaf | ||
|
|
0158a55845 | ||
|
|
657843960c | ||
|
|
fc6a22b072 | ||
|
|
227d99934e | ||
|
|
5036b28e45 | ||
|
|
8b5735c915 | ||
|
|
eb379e1849 | ||
|
|
6a630bc489 | ||
|
|
0d1b474e60 | ||
|
|
84f6cadae7 | ||
|
|
372e740c38 | ||
|
|
57dd979a14 | ||
|
|
c1d07a5471 | ||
|
|
98eddd0532 | ||
|
|
1b21e0d429 | ||
|
|
7776b74126 | ||
|
|
3abb887445 | ||
|
|
a9ec3ae098 | ||
|
|
53927b5a72 | ||
|
|
d67b91d2b0 | ||
|
|
a4fc28abc0 | ||
|
|
bee2a14620 | ||
|
|
168a4dc323 | ||
|
|
47c0decf1e | ||
|
|
8acc818f68 | ||
|
|
9e0468cd73 | ||
|
|
65e9cee019 | ||
|
|
bb3851a35c | ||
|
|
4368bdb2bc | ||
|
|
1b753720c1 | ||
|
|
39e27d2a23 | ||
|
|
608592db0c | ||
|
|
ab1f803354 | ||
|
|
b81c722ac8 | ||
|
|
b40ad399f6 | ||
|
|
dcd18072b4 | ||
|
|
bda2d1d9a4 | ||
|
|
83231b9678 |
@@ -254,7 +254,6 @@ AZURE_APP_SECRET=
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## All javascript files should be reviewed by pranav before merging
|
||||
*.js @pranavrajs
|
||||
*.vue @pranavrajs
|
||||
|
||||
|
||||
## All enterprise related files should be reviewed by sojan before merging
|
||||
/enterprise/* @sojan-official
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for log lines and calculate percentage
|
||||
run: |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# # This action will try to install and setup
|
||||
# # chatwoot on an Ubuntu 20.04 machine using
|
||||
# # the linux installer script.
|
||||
# #
|
||||
# #
|
||||
# # This is set to run daily at midnight.
|
||||
# #
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo ./install.sh --install < input
|
||||
|
||||
# disabling http verify for now as http
|
||||
# disabling http verify for now as http
|
||||
# access to port 3000 fails in gh action env
|
||||
# - name: Verify
|
||||
# if: always()
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# curl http://localhost:3000/api
|
||||
|
||||
- name: Upload chatwoot setup log file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: chatwoot-setup-log-file
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -76,11 +76,11 @@ jobs:
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
|
||||
+11
-70
@@ -2,7 +2,6 @@ require:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
@@ -12,7 +11,8 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -50,6 +50,7 @@ Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
- '**/routes.rb'
|
||||
@@ -102,84 +103,31 @@ RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||
- 'app/models/message.rb'
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161110102609_removeinboxid.rb'
|
||||
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20200121190901_create_account_users.rb'
|
||||
- 'db/migrate/20170211092540_notnullableusers.rb'
|
||||
- 'db/migrate/20170403095203_contactadder.rb'
|
||||
- 'db/migrate/20170406104018_add_default_status_conv.rb'
|
||||
- 'db/migrate/20170511134418_latlong.rb'
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
|
||||
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
|
||||
- 'db/migrate/20200606132552_create_labels.rb'
|
||||
- 'db/migrate/20201027135006_create_working_hours.rb'
|
||||
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
|
||||
- 'db/migrate/20210114202310_create_teams.rb'
|
||||
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
|
||||
- 'db/migrate/20210428135041_add_campaigns.rb'
|
||||
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
|
||||
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
|
||||
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
|
||||
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
|
||||
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
|
||||
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
|
||||
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
|
||||
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
|
||||
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
|
||||
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
|
||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
@@ -187,6 +135,8 @@ RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
@@ -203,13 +153,4 @@ AllCops:
|
||||
- 'config/environments/**/*'
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
||||
- db/migrate/20200503151130_add_account_feature_flag.rb
|
||||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||
- db/migrate/20220809104508_revert_cascading_indexes.rb
|
||||
- 'db/migrate/20230426130150_init_schema.rb'
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 1
|
||||
Lint/DuplicateMethods:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Lint/RescueException:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: AllowKeywordBlockArguments.
|
||||
Lint/UnderscorePrefixedVariableName:
|
||||
Exclude:
|
||||
- 'app/models/account.rb'
|
||||
- 'deploy/before_symlink.rb'
|
||||
|
||||
# Offense count: 18
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/facebook_indicators_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/listeners/reporting_listener.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
|
||||
# Offense count: 14
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
# ExcludedMethods: refine
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
|
||||
# Offense count: 2
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 10
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
|
||||
# Offense count: 1
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 8
|
||||
|
||||
# Offense count: 6
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
||||
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'app/controllers/api/base_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
- 'app/controllers/api/v1/webhooks_controller.rb'
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'lib/integrations/widget/outgoing_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxKeyValuePairs.
|
||||
Performance/RedundantMerge:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Performance/StringReplacement:
|
||||
Exclude:
|
||||
- 'lib/events/base.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: Prefixes.
|
||||
# Prefixes: when, with, without
|
||||
RSpec/ContextWording:
|
||||
Exclude:
|
||||
- 'spec/models/contact_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeClass:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeSymbol:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowConsecutiveOneLiners.
|
||||
RSpec/EmptyLineAfterExample:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Max.
|
||||
RSpec/ExampleLength:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_line_only, single_statement_only, disallow
|
||||
RSpec/ImplicitSubject:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Configuration parameters: AggregateFailuresByDefault.
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: not_to, to_not
|
||||
RSpec/NotToNot:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
Rails/ActiveRecordAliases:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/agents_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/canned_responses_controller.rb'
|
||||
- 'app/controllers/api/v1/contacts_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Rails/BelongsTo:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/EnumHash:
|
||||
Exclude:
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/HasManyOrHasOneDependent:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/InverseOf:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/controllers/**/*.rb
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
Exclude:
|
||||
- 'app/controllers/home_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
- 'app/builders/account_builder.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: strict, flexible
|
||||
Rails/TimeZone:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'lib/reports/update_account_identity.rb'
|
||||
- 'lib/reports/update_agent_identity.rb'
|
||||
- 'lib/reports/update_identity.rb'
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/Validation:
|
||||
Exclude:
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 15
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
- 'app/controllers/api/v1/inbox_members_controller.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/channel/web_widget.rb'
|
||||
- 'app/presenters/conversations/event_data_presenter.rb'
|
||||
- 'app/services/facebook/send_reply_service.rb'
|
||||
- 'lib/integrations/facebook/delivery_status.rb'
|
||||
- 'lib/integrations/facebook/message_creator.rb'
|
||||
- 'lib/integrations/facebook/message_parser.rb'
|
||||
- 'lib/integrations/widget/incoming_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/CommentedKeyword:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/assignments_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/labels_controller.rb'
|
||||
- 'app/controllers/api/v1/labels_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowIfModifier.
|
||||
Style/IfInsideElse:
|
||||
Exclude:
|
||||
- 'app/finders/conversation_finder.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/MixinUsage:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||
# SupportedStyles: predicate, comparison
|
||||
Style/NumericPredicate:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: implicit, explicit
|
||||
Style/RescueStandardError:
|
||||
Exclude:
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
+13
-13
@@ -316,16 +316,16 @@ GEM
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.22.3)
|
||||
google-protobuf (3.22.3-arm64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-linux)
|
||||
google-protobuf (3.25.2)
|
||||
google-protobuf (3.25.2-arm64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.6.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (1.11.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
@@ -335,13 +335,13 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.54.0)
|
||||
grpc (1.54.3)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-darwin)
|
||||
grpc (1.54.3-x86_64-darwin)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-linux)
|
||||
grpc (1.54.3-x86_64-linux)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
@@ -488,14 +488,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.0)
|
||||
nokogiri (1.16.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-arm64-darwin)
|
||||
nokogiri (1.16.2-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-darwin)
|
||||
nokogiri (1.16.2-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
nokogiri (1.16.2-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oauth (1.1.0)
|
||||
|
||||
@@ -15,7 +15,7 @@ class AccountBuilder
|
||||
end
|
||||
[@user, @account]
|
||||
rescue StandardError => e
|
||||
puts e.inspect
|
||||
Rails.logger.debug e.inspect
|
||||
raise e
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# The AgentBuilder class is responsible for creating a new agent.
|
||||
# It initializes with necessary attributes and provides a perform method
|
||||
# to create a user and account user in a transaction.
|
||||
class AgentBuilder
|
||||
# Initializes an AgentBuilder with necessary attributes.
|
||||
# @param email [String] the email of the user.
|
||||
# @param name [String] the name of the user.
|
||||
# @param role [String] the role of the user, defaults to 'agent' if not provided.
|
||||
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
|
||||
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
|
||||
# @param auto_offline [Boolean] the auto offline status of the user.
|
||||
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
|
||||
|
||||
# Creates a user and account user in a transaction.
|
||||
# @return [User] the created user.
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@user = find_or_create_user
|
||||
send_confirmation_if_required
|
||||
create_account_user
|
||||
end
|
||||
@user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Finds a user by email or creates a new one with a temporary password.
|
||||
# @return [User] the found or created user.
|
||||
def find_or_create_user
|
||||
user = User.find_by(email: email)
|
||||
return user if user
|
||||
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
|
||||
end
|
||||
|
||||
# Sends confirmation instructions if the user is persisted and not confirmed.
|
||||
def send_confirmation_if_required
|
||||
@user.send_confirmation_instructions if user_needs_confirmation?
|
||||
end
|
||||
|
||||
# Checks if the user needs confirmation.
|
||||
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
|
||||
def user_needs_confirmation?
|
||||
@user.persisted? && !@user.confirmed?
|
||||
end
|
||||
|
||||
# Creates an account user linking the user to the current account.
|
||||
def create_account_user
|
||||
AccountUser.create!({
|
||||
account_id: account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: inviter.id
|
||||
}.merge({
|
||||
role: role,
|
||||
availability: availability,
|
||||
auto_offline: auto_offline
|
||||
}.compact))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:user_id
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class V2::Reports::BaseSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
private
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
'conversations.team_id'
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.each_with_object([]) do |team, arr|
|
||||
arr << {
|
||||
id: team.id,
|
||||
conversations_count: @grouped_conversations_count[team.id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,26 @@
|
||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
||||
before_action :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :create_user, only: [:create]
|
||||
before_action :save_account_user, only: [:create]
|
||||
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
|
||||
|
||||
def index
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
builder = AgentBuilder.new(
|
||||
email: new_agent_params['email'],
|
||||
name: new_agent_params['name'],
|
||||
role: new_agent_params['role'],
|
||||
availability: new_agent_params['availability'],
|
||||
auto_offline: new_agent_params['auto_offline'],
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
|
||||
builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@@ -23,6 +33,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def bulk_create
|
||||
emails = params[:emails]
|
||||
|
||||
emails.each do |email|
|
||||
builder = AgentBuilder.new(
|
||||
email: email,
|
||||
name: email.split('@').first,
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
begin
|
||||
builder.perform
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -33,47 +62,34 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.find_by(email: new_agent_params[:email])
|
||||
end
|
||||
|
||||
# TODO: move this to a builder and combine the save account user method into a builder
|
||||
# ensure the account user association is also created in a single transaction
|
||||
def create_user
|
||||
return @user.send_confirmation_instructions if @user
|
||||
|
||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: current_user.id
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def validate_limit_for_bulk_create
|
||||
limit_available = params[:emails].count <= available_agent_count
|
||||
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent?
|
||||
end
|
||||
|
||||
def available_agent_count
|
||||
Current.account.usage_limits[:agents] - agents.count
|
||||
end
|
||||
|
||||
def can_add_agent?
|
||||
available_agent_count.positive?
|
||||
end
|
||||
|
||||
def delete_user_record(agent)
|
||||
|
||||
@@ -7,8 +7,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@notifications = notification_finder.notifications
|
||||
@unread_count = notification_finder.unread_count
|
||||
@notifications = notification_finder.perform
|
||||
@count = notification_finder.count
|
||||
end
|
||||
|
||||
@@ -39,6 +39,15 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
if params[:type] == 'read'
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
|
||||
else
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@unread_count = notification_finder.unread_count
|
||||
render json: @unread_count
|
||||
|
||||
@@ -40,11 +40,13 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
def cache_keys
|
||||
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
render json: { cache_keys: cache_keys_for_account }, status: :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
@@ -66,7 +68,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
raise CustomExceptions::Account::InvalidParams.new({})
|
||||
end
|
||||
|
||||
def get_cache_keys
|
||||
def cache_keys_for_account
|
||||
{
|
||||
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
||||
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
||||
@@ -83,6 +85,10 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.permit(:industry, :company_size, :timezone)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
@@ -10,7 +10,9 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(password_params.except(:current_password))
|
||||
end
|
||||
|
||||
@user.update!(profile_params)
|
||||
@user.assign_attributes(profile_params)
|
||||
@user.custom_attributes.merge!(custom_attributes_params)
|
||||
@user.save!
|
||||
end
|
||||
|
||||
def avatar
|
||||
@@ -62,6 +64,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.require(:profile).permit(:phone_number)
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:profile).permit(
|
||||
:current_password,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
end
|
||||
|
||||
def team
|
||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize :report, :view?
|
||||
end
|
||||
|
||||
def prepare_builder_params
|
||||
@builder_params = {
|
||||
since: permitted_params[:since],
|
||||
until: permitted_params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:since, :until, :business_hours)
|
||||
end
|
||||
end
|
||||
@@ -17,8 +17,12 @@ class Api::V2::AccountsController < Api::BaseController
|
||||
@user, @account = AccountBuilder.new(
|
||||
email: account_params[:email],
|
||||
user_password: account_params[:password],
|
||||
locale: account_params[:locale],
|
||||
user: current_user
|
||||
).perform
|
||||
|
||||
fetch_account_and_user_info
|
||||
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||
@@ -29,6 +33,8 @@ class Api::V2::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def fetch_account_and_user_info; end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
@@ -46,3 +52,5 @@ class Api::V2::AccountsController < Api::BaseController
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::AccountsController.prepend_mod_with('Api::V2::AccountsController')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => %w[toggle_status create],
|
||||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create],
|
||||
'api/v1/accounts/conversations/messages' => ['create'],
|
||||
'api/v1/accounts/conversations/assignments' => ['create']
|
||||
}.freeze
|
||||
|
||||
def ensure_access_token
|
||||
|
||||
@@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,8 +28,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
end
|
||||
|
||||
def sha
|
||||
sha = `git rev-parse HEAD`
|
||||
@metrics['Git SHA'] = sha.presence || 'n/a'
|
||||
@metrics['Git SHA'] = GIT_HASH
|
||||
end
|
||||
|
||||
def postgres_status
|
||||
|
||||
@@ -169,6 +169,12 @@ class ConversationFinder
|
||||
)
|
||||
|
||||
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
@conversations = @conversations.send(sort_by, sort_order)
|
||||
|
||||
if params[:updated_within].present?
|
||||
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
|
||||
else
|
||||
@conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,8 @@ class NotificationFinder
|
||||
set_up
|
||||
end
|
||||
|
||||
def perform
|
||||
notifications
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@@ -26,22 +26,31 @@ class NotificationFinder
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_by_status
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||
def filter_snoozed_notifications
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def fitler_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
def type_included?(type)
|
||||
(params[:includes] || []).include?(type)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
|
||||
def sort_order
|
||||
params[:sort_order] || :desc
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
dates = data.pluck(:date).uniq.sort
|
||||
|
||||
# add the dates as the first row, leave an empty cell for the hour column
|
||||
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << ([nil] + dates)
|
||||
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << (['Start of the hour'] + dates)
|
||||
|
||||
# group the data by hour, we do not need to sort it, because the data is already sorted
|
||||
# given it starts from the beginning of the day
|
||||
@@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
||||
data.group_by { |d| d[:hour] }.each do |hour, items|
|
||||
# create a new row for each hour
|
||||
row = [hour]
|
||||
row = [format('%02d:00', hour)]
|
||||
|
||||
# group the items by date, so we can easily access the value for each date
|
||||
# grouped values will be a hasg with the date as the key, and the value as the value
|
||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
||||
end
|
||||
|
||||
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
|
||||
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4]
|
||||
# add the row to the result array
|
||||
|
||||
result_arr << row
|
||||
@@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# return the resultant array
|
||||
# the result looks like this
|
||||
# [
|
||||
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# [0, 0, 0, 0],
|
||||
# [1, 0, 0, 0],
|
||||
# [2, 0, 0, 0],
|
||||
# [3, 0, 0, 0],
|
||||
# [4, 0, 0, 0],
|
||||
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# ['00:00', 0, 0, 0],
|
||||
# ['01:00', 0, 0, 0],
|
||||
# ['02:00', 0, 0, 0],
|
||||
# ['03:00', 0, 0, 0],
|
||||
# ['04:00', 0, 0, 0],
|
||||
# ]
|
||||
result_arr
|
||||
end
|
||||
|
||||
@@ -45,12 +45,8 @@ module Api::V2::Accounts::ReportsHelper
|
||||
def generate_readable_report_metrics(report_metric)
|
||||
[
|
||||
report_metric[:conversations_count],
|
||||
time_to_minutes(report_metric[:avg_first_response_time]),
|
||||
time_to_minutes(report_metric[:avg_resolution_time])
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format
|
||||
]
|
||||
end
|
||||
|
||||
def time_to_minutes(time_in_seconds)
|
||||
(time_in_seconds / 60).to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
|
||||
bulkInvite({ emails }) {
|
||||
return axios.post(`${this.url}/bulk_create`, {
|
||||
emails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Agents();
|
||||
|
||||
@@ -6,8 +6,16 @@ class NotificationsAPI extends ApiClient {
|
||||
super('notifications', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page) {
|
||||
return axios.get(`${this.url}?page=${page}`);
|
||||
get({ page, status, type, sortOrder }) {
|
||||
const includesFilter = [status, type].filter(value => !!value);
|
||||
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
sort_order: sortOrder,
|
||||
includes: includesFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(contactId) {
|
||||
@@ -36,6 +44,18 @@ class NotificationsAPI extends ApiClient {
|
||||
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();
|
||||
|
||||
@@ -10,4 +10,29 @@ describe('#AgentAPI', () => {
|
||||
expect(agents).toHaveProperty('update');
|
||||
expect(agents).toHaveProperty('delete');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#bulkInvite', () => {
|
||||
agents.bulkInvite({ emails: ['hello@hi.com'] });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/agents/bulk_create',
|
||||
{
|
||||
emails: ['hello@hi.com'],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,11 +27,37 @@ describe('#NotificationAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get(1);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications?page=1'
|
||||
);
|
||||
describe('#get', () => {
|
||||
it('generates the API call if both params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'snoozed',
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['snoozed', 'read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the API call if one of the params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('#getNotifications', () => {
|
||||
@@ -65,5 +91,30 @@ describe('#NotificationAPI', () => {
|
||||
'/api/v1/notifications/read_all'
|
||||
);
|
||||
});
|
||||
|
||||
it('#snooze', () => {
|
||||
notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/1/snooze',
|
||||
{
|
||||
snoozed_until: 12332211,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
notificationsAPI.delete(1);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1');
|
||||
});
|
||||
|
||||
it('#deleteAll', () => {
|
||||
notificationsAPI.deleteAll({ type: 'all' });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/destroy_all',
|
||||
{
|
||||
type: 'all',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -753,7 +753,7 @@ export default {
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
@apply z-50 bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800;
|
||||
@apply z-[9999] bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800 shadow-lg;
|
||||
|
||||
h5 {
|
||||
@apply dark:text-slate-25 text-slate-800;
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0"
|
||||
class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0 rtl:relative rtl:left-6"
|
||||
:class="{ 'justify-end': isContactPanelOpen }"
|
||||
>
|
||||
<more-actions :conversation-id="currentChat.id" />
|
||||
@@ -86,7 +86,7 @@ import MoreActions from './MoreActions.vue';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
if (snoozedUntil) {
|
||||
return `${this.$t(
|
||||
'CONVERSATION.HEADER.SNOOZED_UNTIL'
|
||||
)} ${conversationReopenTime(snoozedUntil)}`;
|
||||
)} ${snoozedReopenTime(snoozedUntil)}`;
|
||||
}
|
||||
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
|
||||
},
|
||||
|
||||
@@ -46,5 +46,18 @@ export default {
|
||||
},
|
||||
EXAMPLE_URL: 'https://example.com',
|
||||
EXAMPLE_WEBHOOK_URL: 'https://example/api/webhook',
|
||||
INBOX_SORT_BY: {
|
||||
NEWEST: 'desc',
|
||||
OLDEST: 'asc',
|
||||
},
|
||||
INBOX_DISPLAY_BY: {
|
||||
SNOOZED: 'snoozed',
|
||||
READ: 'read',
|
||||
},
|
||||
INBOX_FILTER_TYPE: {
|
||||
STATUS: 'status',
|
||||
TYPE: 'type',
|
||||
SORT_ORDER: 'sort_order',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
@@ -24,6 +24,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'conversation.mentioned': this.onConversationMentioned,
|
||||
'notification.created': this.onNotificationCreated,
|
||||
'notification.deleted': this.onNotificationDeleted,
|
||||
'notification.updated': this.onNotificationUpdated,
|
||||
'first.reply.created': this.onFirstReplyCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
'conversation.updated': this.onConversationUpdated,
|
||||
@@ -200,6 +201,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
this.app.$store.dispatch('notifications/deleteNotification', data);
|
||||
};
|
||||
|
||||
onNotificationUpdated = data => {
|
||||
this.app.$store.dispatch('notifications/updateNotification', data);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onFirstReplyCreated = () => {
|
||||
bus.$emit('fetch_overview_reports');
|
||||
|
||||
@@ -86,3 +86,6 @@ export const getConversationDashboardRoute = routeName => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const isAInboxViewRoute = routeName =>
|
||||
['inbox_view_conversation'].includes(routeName);
|
||||
|
||||
@@ -55,7 +55,7 @@ export const findSnoozeTime = (snoozeType, currentDate = new Date()) => {
|
||||
|
||||
return parsedDate ? getUnixTime(parsedDate) : null;
|
||||
};
|
||||
export const conversationReopenTime = snoozedUntil => {
|
||||
export const snoozedReopenTime = snoozedUntil => {
|
||||
if (!snoozedUntil) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isAConversationRoute,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
} from '../routeHelpers';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
@@ -134,3 +135,10 @@ describe('getConversationDashboardRoute', () => {
|
||||
expect(getConversationDashboardRoute('non_existent_route')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAInboxViewRoute', () => {
|
||||
it('returns true if inbox view route name is provided', () => {
|
||||
expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true);
|
||||
expect(isAInboxViewRoute('inbox_conversation')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
findSnoozeTime,
|
||||
conversationReopenTime,
|
||||
snoozedReopenTime,
|
||||
findStartOfNextWeek,
|
||||
findStartOfNextMonth,
|
||||
findNextDay,
|
||||
@@ -88,13 +88,13 @@ describe('#Snooze Helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('conversationReopenTime', () => {
|
||||
describe('snoozedReopenTime', () => {
|
||||
it('should return nil if snoozedUntil is nil', () => {
|
||||
expect(conversationReopenTime(null)).toEqual(null);
|
||||
expect(snoozedReopenTime(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return formatted date if snoozedUntil is not nil', () => {
|
||||
expect(conversationReopenTime('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
expect(snoozedReopenTime('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
'7 Jun, 9.00am'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,7 +112,8 @@
|
||||
"REMOVE_LABEL": "Remove label from the conversation",
|
||||
"SETTINGS": "Settings",
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"APPEARANCE": "Appearance"
|
||||
"APPEARANCE": "Appearance",
|
||||
"SNOOZE_NOTIFICATION": "Snooze Notification"
|
||||
},
|
||||
"COMMANDS": {
|
||||
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||
@@ -153,7 +154,8 @@
|
||||
"CHANGE_APPEARANCE": "Change Appearance",
|
||||
"LIGHT_MODE": "Light",
|
||||
"DARK_MODE": "Dark",
|
||||
"SYSTEM_MODE": "System"
|
||||
"SYSTEM_MODE": "System",
|
||||
"SNOOZE_NOTIFICATION": "Snooze Notification"
|
||||
}
|
||||
},
|
||||
"DASHBOARD_APPS": {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
"LOADING": "Fetching notifications",
|
||||
"EOF": "All notifications loaded 🎉",
|
||||
"404": "There are no active notifications in this group.",
|
||||
"NOTE": "Notifications from all subscribed inboxes"
|
||||
"NO_NOTIFICATIONS": "No notifications",
|
||||
"NOTE": "Notifications from all subscribed inboxes",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week"
|
||||
},
|
||||
"ACTION_HEADER": {
|
||||
"SNOOZE": "Snooze notification",
|
||||
@@ -42,6 +46,15 @@
|
||||
"LABELS": "Labels",
|
||||
"CONVERSATION_ID": "Conversation ID"
|
||||
}
|
||||
},
|
||||
"ALERTS": {
|
||||
"MARK_AS_READ": "Notification marked as read",
|
||||
"MARK_AS_UNREAD": "Notification marked as unread",
|
||||
"SNOOZE": "Notification snoozed",
|
||||
"DELETE": "Notification deleted",
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,3 +71,5 @@ export const ICON_APPEARANCE = `<svg role="img" class="ninja-icon ninja-icon--fl
|
||||
export const ICON_LIGHT_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2Zm5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0Zm4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5ZM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19Zm-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5Zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06Zm1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06l-1.5 1.5Zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06Zm-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06l1.5 1.5Z"/></svg>`;
|
||||
export const ICON_DARK_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A9.965 9.965 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.232-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.961 9.961 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662Zm-8.248-4.903c-1.25 2.389-3.31 4.1-6.817 5.499a8.49 8.49 0 0 0 2.152 1.766a8.502 8.502 0 0 0 8.502-14.725a8.484 8.484 0 0 0-2.792-1.015c.647 3.384.23 6.043-1.045 8.475Z"/></svg>`;
|
||||
export const ICON_SYSTEM_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M4.25 3A2.25 2.25 0 0 0 2 5.25v10.5A2.25 2.25 0 0 0 4.25 18H9.5v1.25c0 .69-.56 1.25-1.25 1.25h-.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-.5c-.69 0-1.25-.56-1.25-1.25V18h5.25A2.25 2.25 0 0 0 22 15.75V5.25A2.25 2.25 0 0 0 19.75 3H4.25ZM13 18v1.25c0 .45.108.875.3 1.25h-2.6c.192-.375.3-.8.3-1.25V18h2ZM3.5 5.25a.75.75 0 0 1 .75-.75h15.5a.75.75 0 0 1 .75.75V13h-17V5.25Zm0 9.25h17v1.25a.75.75 0 0 1-.75.75H4.25a.75.75 0 0 1-.75-.75V14.5Z"/></svg>`;
|
||||
|
||||
export const ICON_SNOOZE_NOTIFICATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3.5c-3.104 0-6 2.432-6 6.25v4.153L4.682 17h14.67l-1.354-3.093V11.75a.75.75 0 0 1 1.5 0v1.843l1.381 3.156a1.25 1.25 0 0 1-1.145 1.751H15a3.002 3.002 0 0 1-6.003 0H4.305a1.25 1.25 0 0 1-1.15-1.739l1.344-3.164V9.75C4.5 5.068 8.103 2 12 2c.86 0 1.705.15 2.5.432a.75.75 0 0 1-.502 1.413A5.964 5.964 0 0 0 12 3.5ZM12 20c.828 0 1.5-.671 1.501-1.5h-3.003c0 .829.673 1.5 1.502 1.5Zm3.25-13h-2.5l-.101.007A.75.75 0 0 0 12.75 8.5h1.043l-1.653 2.314l-.055.09A.75.75 0 0 0 12.75 12h2.5l.102-.007a.75.75 0 0 0-.102-1.493h-1.042l1.653-2.314l.055-.09A.75.75 0 0 0 15.25 7Zm6-5h-3.5l-.101.007A.75.75 0 0 0 17.75 3.5h2.134l-2.766 4.347l-.05.09A.75.75 0 0 0 17.75 9h3.5l.102-.007A.75.75 0 0 0 21.25 7.5h-2.133l2.766-4.347l.05-.09A.75.75 0 0 0 21.25 2Z"/></svg>`;
|
||||
|
||||
@@ -15,3 +15,6 @@ export const CMD_REOPEN_CONVERSATION = 'CMD_REOPEN_CONVERSATION';
|
||||
export const CMD_RESOLVE_CONVERSATION = 'CMD_RESOLVE_CONVERSATION';
|
||||
export const CMD_SNOOZE_CONVERSATION = 'CMD_SNOOZE_CONVERSATION';
|
||||
export const CMD_AI_ASSIST = 'CMD_AI_ASSIST';
|
||||
|
||||
// Inbox Commands (Notifications)
|
||||
export const CMD_SNOOZE_NOTIFICATION = 'CMD_SNOOZE_NOTIFICATION';
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script>
|
||||
import 'ninja-keys';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import inboxHotKeysMixin from './inboxHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
@@ -25,6 +26,7 @@ export default {
|
||||
adminMixin,
|
||||
agentMixin,
|
||||
conversationHotKeysMixin,
|
||||
inboxHotKeysMixin,
|
||||
conversationLabelMixin,
|
||||
conversationTeamMixin,
|
||||
appearanceHotKeys,
|
||||
@@ -42,6 +44,7 @@ export default {
|
||||
},
|
||||
hotKeys() {
|
||||
return [
|
||||
...this.inboxHotKeys,
|
||||
...this.conversationHotKeys,
|
||||
...this.goToCommandHotKeys,
|
||||
...this.goToAppearanceHotKeys,
|
||||
|
||||
@@ -30,7 +30,10 @@ import {
|
||||
UNMUTE_ACTION,
|
||||
MUTE_ACTION,
|
||||
} from './commandBarActions';
|
||||
import { isAConversationRoute } from '../../../helper/routeHelpers';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
} from '../../../helper/routeHelpers';
|
||||
export default {
|
||||
mixins: [aiMixin],
|
||||
watch: {
|
||||
@@ -325,7 +328,10 @@ export default {
|
||||
},
|
||||
|
||||
conversationHotKeys() {
|
||||
if (isAConversationRoute(this.$route.name)) {
|
||||
if (
|
||||
isAConversationRoute(this.$route.name) ||
|
||||
isAInboxViewRoute(this.$route.name)
|
||||
) {
|
||||
const defaultConversationHotKeys = [
|
||||
...this.statusActions,
|
||||
...this.conversationAdditionalActions,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import { CMD_SNOOZE_NOTIFICATION } from './commandBarBusEvents';
|
||||
import { ICON_SNOOZE_NOTIFICATION } from './CommandBarIcons';
|
||||
|
||||
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
const INBOX_SNOOZE_EVENTS = [
|
||||
{
|
||||
id: 'snooze_notification',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_NOTIFICATION',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.AN_HOUR_FROM_NOW,
|
||||
title: 'COMMAND_BAR.COMMANDS.AN_HOUR_FROM_NOW',
|
||||
parent: 'snooze_notification',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_TOMORROW,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_TOMORROW',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_WEEK',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_MONTH',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
|
||||
title: 'COMMAND_BAR.COMMANDS.CUSTOM',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
},
|
||||
];
|
||||
export default {
|
||||
computed: {
|
||||
inboxHotKeys() {
|
||||
if (isAInboxViewRoute(this.$route.name)) {
|
||||
return this.prepareActions(INBOX_SNOOZE_EVENTS);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prepareActions(actions) {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: this.$t(action.title),
|
||||
section: this.$t(action.section),
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -182,6 +182,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
getConversationDashboardRoute,
|
||||
} from '../../../../helper/routeHelpers';
|
||||
|
||||
@@ -303,6 +304,10 @@ export default {
|
||||
this.$router.push({
|
||||
name: getConversationDashboardRoute(this.$route.name),
|
||||
});
|
||||
} else if (isAInboxViewRoute(this.$route.name)) {
|
||||
this.$router.push({
|
||||
name: 'inbox_view',
|
||||
});
|
||||
} else if (this.$route.name !== 'contacts_dashboard') {
|
||||
this.$router.push({
|
||||
name: 'contacts_dashboard',
|
||||
|
||||
@@ -6,8 +6,8 @@ const InboxView = () => import('../inbox/InboxView.vue');
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox'),
|
||||
name: 'inbox',
|
||||
path: frontendURL('accounts/:accountId/inbox-view'),
|
||||
name: 'inbox_view',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: InboxView,
|
||||
props: () => {
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox/:conversation_id'),
|
||||
path: frontendURL('accounts/:accountId/inbox-view/:conversation_id'),
|
||||
name: 'inbox_view_conversation',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: InboxView,
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
class="flex flex-col h-full w-full ltr:border-r border-slate-50 dark:border-slate-800/50"
|
||||
:class="isOnExpandedLayout ? '' : 'min-w-[360px] max-w-[360px]'"
|
||||
>
|
||||
<inbox-list-header />
|
||||
<inbox-list-header @filter="onFilterChange" />
|
||||
<div
|
||||
ref="notificationList"
|
||||
class="flex flex-col w-full h-[calc(100%-56px)] overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<inbox-card
|
||||
v-for="notificationItem in records"
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@mark-notification-as-read="markNotificationAsRead"
|
||||
@@ -20,8 +20,14 @@
|
||||
<span class="spinner mt-4 mb-4" />
|
||||
</div>
|
||||
<p
|
||||
v-if="showEndOfList"
|
||||
class="text-center text-slate-300 dark:text-slate-400 p-4"
|
||||
v-if="showEmptyState"
|
||||
class="text-center text-slate-400 text-sm dark:text-slate-400 p-4 font-medium"
|
||||
>
|
||||
{{ $t('INBOX.LIST.NO_NOTIFICATIONS') }}
|
||||
</p>
|
||||
<p
|
||||
v-if="showEndOfListMessage"
|
||||
class="text-center text-slate-400 dark:text-slate-400 p-4"
|
||||
>
|
||||
{{ $t('INBOX.LIST.EOF') }}
|
||||
</p>
|
||||
@@ -36,16 +42,20 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import InboxCard from './components/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
export default {
|
||||
components: {
|
||||
InboxCard,
|
||||
InboxListHeader,
|
||||
IntersectionObserver,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
@@ -63,36 +73,64 @@ export default {
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
page: 1,
|
||||
status: '',
|
||||
type: '',
|
||||
sortOrder: wootConstants.INBOX_SORT_BY.NEWEST,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
meta: 'notifications/getMeta',
|
||||
records: 'notifications/getNotifications',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
notification: 'notifications/getFilteredNotifications',
|
||||
}),
|
||||
inboxFilters() {
|
||||
return {
|
||||
page: this.page,
|
||||
status: this.status,
|
||||
type: this.type,
|
||||
sortOrder: this.sortOrder,
|
||||
};
|
||||
},
|
||||
notifications() {
|
||||
return this.notification(this.inboxFilters);
|
||||
},
|
||||
showEndOfList() {
|
||||
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
||||
},
|
||||
showEmptyState() {
|
||||
return !this.uiFlags.isFetching && !this.notifications.length;
|
||||
},
|
||||
showEndOfListMessage() {
|
||||
return this.showEndOfList && this.notifications.length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('notifications/clear');
|
||||
this.$store.dispatch('notifications/index', { page: 1 });
|
||||
this.setSavedFilter();
|
||||
this.fetchNotifications();
|
||||
},
|
||||
methods: {
|
||||
fetchNotifications() {
|
||||
this.page = 1;
|
||||
this.$store.dispatch('notifications/clear');
|
||||
const filter = this.inboxFilters;
|
||||
|
||||
this.$store.dispatch('notifications/index', filter);
|
||||
},
|
||||
redirectToInbox() {
|
||||
if (!this.conversationId) return;
|
||||
if (this.$route.name === 'inbox') return;
|
||||
this.$router.push({ name: 'inbox' });
|
||||
},
|
||||
onMarkAllDoneClick() {
|
||||
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
|
||||
this.$store.dispatch('notifications/readAll');
|
||||
if (this.$route.name === 'inbox_view') return;
|
||||
this.$router.push({ name: 'inbox_view' });
|
||||
},
|
||||
loadMoreNotifications() {
|
||||
if (this.uiFlags.isAllNotificationsLoaded) return;
|
||||
this.$store.dispatch('notifications/index', { page: this.page + 1 });
|
||||
this.$store.dispatch('notifications/index', {
|
||||
page: this.page + 1,
|
||||
status: this.status,
|
||||
type: this.type,
|
||||
sortOrder: this.sortOrder,
|
||||
});
|
||||
this.page += 1;
|
||||
},
|
||||
markNotificationAsRead(notification) {
|
||||
@@ -102,29 +140,60 @@ export default {
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
} = notification;
|
||||
this.$store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
});
|
||||
this.$store
|
||||
.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
});
|
||||
},
|
||||
markNotificationAsUnRead(notification) {
|
||||
this.$track(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
||||
this.redirectToInbox();
|
||||
const { id } = notification;
|
||||
this.$store.dispatch('notifications/unread', {
|
||||
id,
|
||||
});
|
||||
this.$store
|
||||
.dispatch('notifications/unread', {
|
||||
id,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
});
|
||||
},
|
||||
deleteNotification(notification) {
|
||||
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||
this.redirectToInbox();
|
||||
this.$store.dispatch('notifications/delete', {
|
||||
notification,
|
||||
unread_count: this.meta.unreadCount,
|
||||
count: this.meta.count,
|
||||
});
|
||||
this.$store
|
||||
.dispatch('notifications/delete', {
|
||||
notification,
|
||||
unread_count: this.meta.unreadCount,
|
||||
count: this.meta.count,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.DELETE'));
|
||||
});
|
||||
},
|
||||
onFilterChange(option) {
|
||||
if (option.type === wootConstants.INBOX_FILTER_TYPE.STATUS) {
|
||||
this.status = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === wootConstants.INBOX_FILTER_TYPE.TYPE) {
|
||||
this.type = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === wootConstants.INBOX_FILTER_TYPE.SORT_ORDER) {
|
||||
this.sortOrder = option.key;
|
||||
}
|
||||
this.fetchNotifications();
|
||||
},
|
||||
setSavedFilter() {
|
||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { status, type, sort_by: sortBy } = filterBy;
|
||||
this.status = status;
|
||||
this.type = type;
|
||||
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
:conversation-id="conversationId"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
/>
|
||||
<div v-if="showInboxMessageView" class="flex flex-col w-full h-full">
|
||||
<div
|
||||
v-if="showInboxMessageView"
|
||||
class="flex flex-col h-full"
|
||||
:class="isOnExpandedLayout ? 'w-full' : 'w-[calc(100%-360px)]'"
|
||||
>
|
||||
<inbox-item-header
|
||||
:total-length="totalNotifications"
|
||||
:current-index="activeNotificationIndex"
|
||||
:active-notification="activeNotification"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
@@ -26,13 +31,13 @@
|
||||
class="text-center bg-slate-25 dark:bg-slate-800 justify-center w-full h-full flex items-center"
|
||||
>
|
||||
<span v-if="uiFlags.isFetching" class="spinner mt-4 mb-4" />
|
||||
<div v-else class="flex flex-row items-center gap-1">
|
||||
<div v-else class="flex flex-col items-center gap-2">
|
||||
<fluent-icon
|
||||
icon="mail-inbox"
|
||||
size="18"
|
||||
class="text-slate-700 dark:text-slate-400"
|
||||
size="40"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
<span class="text-slate-700 text-sm font-medium dark:text-slate-400">
|
||||
<span class="text-slate-500 text-sm font-medium dark:text-slate-300">
|
||||
{{ $t('INBOX.LIST.NOTE') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -76,6 +81,11 @@ export default {
|
||||
allConversation: 'getAllConversations',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
}),
|
||||
activeNotification() {
|
||||
return this.notifications.find(
|
||||
n => n.primary_actor.id === Number(this.conversationId)
|
||||
);
|
||||
},
|
||||
isInboxViewEnabled() {
|
||||
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
|
||||
this.currentAccountId,
|
||||
@@ -138,6 +148,7 @@ export default {
|
||||
name: 'home',
|
||||
});
|
||||
}
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
async fetchConversationById() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-col pl-5 pr-3 gap-2.5 py-3 w-full border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
class="flex flex-col ltr:pl-5 rtl:pl-3 rtl:pr-5 ltr:pr-3 gap-2.5 py-3 w-full border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
:class="
|
||||
isInboxCardActive
|
||||
? 'bg-slate-25 dark:bg-slate-800 click-animation'
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div
|
||||
v-if="isUnread"
|
||||
class="absolute -left-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
/>
|
||||
<InboxNameAndId :inbox="inbox" :conversation-id="primaryActor.id" />
|
||||
|
||||
@@ -29,15 +29,15 @@
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
:username="assigneeMeta.name"
|
||||
size="20px"
|
||||
size="16px"
|
||||
class="relative bottom-0.5"
|
||||
/>
|
||||
<div class="flex min-w-0">
|
||||
<span
|
||||
class="font-medium text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
class="text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="isUnread ? 'font-medium' : 'font-normal'"
|
||||
>
|
||||
<span class="font-normal text-sm">
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,6 +47,11 @@
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="snoozedUntilTime" class="flex items-center">
|
||||
<span class="text-woot-500 dark:text-woot-500 text-xs font-medium">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
</div>
|
||||
<inbox-context-menu
|
||||
v-if="isContextMenuOpen"
|
||||
:context-menu-position="contextMenuPosition"
|
||||
@@ -63,6 +68,7 @@ import InboxNameAndId from './InboxNameAndId.vue';
|
||||
import InboxContextMenu from './InboxContextMenu.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
components: {
|
||||
@@ -138,6 +144,18 @@ export default {
|
||||
}
|
||||
return items;
|
||||
},
|
||||
snoozedUntilTime() {
|
||||
const { snoozed_until: snoozedUntil } = this.notificationItem;
|
||||
return snoozedUntil;
|
||||
},
|
||||
snoozedDisplayText() {
|
||||
if (this.snoozedUntilTime) {
|
||||
return `${this.$t('INBOX.LIST.SNOOZED_UNTIL')} ${snoozedReopenTime(
|
||||
this.snoozedUntilTime
|
||||
)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
this.closeContextMenu();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@close="handleClose"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-500 rounded-xl"
|
||||
class="bg-white dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl"
|
||||
>
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
|
||||
@@ -38,18 +38,17 @@
|
||||
:key="option.key"
|
||||
role="button"
|
||||
class="flex rounded-[4px] h-5 w-full items-center justify-between p-0.5 gap-1"
|
||||
:class="
|
||||
activeSort === option.key ? 'bg-woot-50 dark:bg-woot-700/50' : ''
|
||||
"
|
||||
@click.stop="onSortOptionClick(option.key)"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-woot-700/50': activeSort === option.key,
|
||||
}"
|
||||
@click.stop="onSortOptionClick(option)"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium hover:text-woot-600 dark:hover:text-woot-600"
|
||||
:class="
|
||||
activeSort === option.key
|
||||
? 'text-woot-600 dark:text-woot-600'
|
||||
: 'text-slate-600 dark:text-slate-300'
|
||||
"
|
||||
:class="{
|
||||
'text-woot-600 dark:text-woot-600': activeSort === option.key,
|
||||
'text-slate-600 dark:text-slate-300': activeSort !== option.key,
|
||||
}"
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
@@ -74,19 +73,19 @@
|
||||
>
|
||||
<div
|
||||
v-for="option in displayOptions"
|
||||
:key="option.id"
|
||||
:key="option.key"
|
||||
class="flex items-center px-3 py-2 gap-1.5 h-9"
|
||||
>
|
||||
<input
|
||||
:id="option.value"
|
||||
:id="option.key"
|
||||
type="checkbox"
|
||||
:name="option.value"
|
||||
:name="option.key"
|
||||
:checked="option.selected"
|
||||
class="m-0 border-[1.5px] shadow border-slate-200 dark:border-slate-600 appearance-none rounded-[4px] w-4 h-4 dark:bg-slate-800 focus:ring-1 focus:ring-slate-100 dark:focus:ring-slate-700 checked:bg-woot-600 dark:checked:bg-woot-600 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center checked:border-t checked:border-woot-700 dark:checked:border-woot-300 checked:border-b-0 checked:border-r-0 checked:border-l-0 after:text-center after:text-xs after:font-bold after:relative after:-top-[1.5px]"
|
||||
@change="updateDisplayOption(option)"
|
||||
/>
|
||||
<label
|
||||
:for="option.value"
|
||||
:for="option.key"
|
||||
class="text-xs font-medium text-slate-800 !ml-0 !mr-0 dark:text-slate-100"
|
||||
>
|
||||
{{ option.name }}
|
||||
@@ -98,71 +97,101 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
mixins: [uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
showSortMenu: false,
|
||||
displayOptions: [
|
||||
{
|
||||
id: 1,
|
||||
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.SNOOZED'),
|
||||
value: 'snoozed',
|
||||
key: wootConstants.INBOX_DISPLAY_BY.SNOOZED,
|
||||
selected: false,
|
||||
type: wootConstants.INBOX_FILTER_TYPE.STATUS,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.READ'),
|
||||
value: 'read',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.LABELS'),
|
||||
value: 'labels',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.CONVERSATION_ID'),
|
||||
value: 'conversationId',
|
||||
key: wootConstants.INBOX_DISPLAY_BY.READ,
|
||||
selected: false,
|
||||
type: wootConstants.INBOX_FILTER_TYPE.TYPE,
|
||||
},
|
||||
],
|
||||
sortOptions: [
|
||||
{
|
||||
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.NEWEST'),
|
||||
key: 'newest',
|
||||
key: wootConstants.INBOX_SORT_BY.NEWEST,
|
||||
type: wootConstants.INBOX_FILTER_TYPE.SORT_ORDER,
|
||||
},
|
||||
{
|
||||
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.OLDEST'),
|
||||
key: 'oldest',
|
||||
},
|
||||
{
|
||||
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.PRIORITY'),
|
||||
key: 'priority',
|
||||
key: wootConstants.INBOX_SORT_BY.OLDEST,
|
||||
type: wootConstants.INBOX_FILTER_TYPE.SORT_ORDER,
|
||||
},
|
||||
],
|
||||
activeSort: 'newest',
|
||||
activeSort: wootConstants.INBOX_SORT_BY.NEWEST,
|
||||
activeDisplayFilter: {
|
||||
status: '',
|
||||
type: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeSortOption() {
|
||||
return this.sortOptions.find(option => option.key === this.activeSort)
|
||||
.name;
|
||||
return (
|
||||
this.sortOptions.find(option => option.key === this.activeSort)?.name ||
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSavedFilter();
|
||||
},
|
||||
methods: {
|
||||
updateDisplayOption(option) {
|
||||
option.selected = !option.selected;
|
||||
// TODO: Update the display options
|
||||
this.displayOptions.forEach(displayOption => {
|
||||
if (displayOption.key === option.key) {
|
||||
displayOption.selected = !option.selected;
|
||||
this.activeDisplayFilter[displayOption.type] = displayOption.selected
|
||||
? displayOption.key
|
||||
: '';
|
||||
this.saveSelectedDisplayFilter();
|
||||
this.$emit('filter', option);
|
||||
}
|
||||
});
|
||||
},
|
||||
openSortMenu() {
|
||||
this.showSortMenu = !this.showSortMenu;
|
||||
},
|
||||
onSortOptionClick(key) {
|
||||
this.activeSort = key;
|
||||
onSortOptionClick(option) {
|
||||
this.activeSort = option.key;
|
||||
this.showSortMenu = false;
|
||||
// TODO: Update the sort options
|
||||
this.saveSelectedDisplayFilter();
|
||||
this.$emit('filter', option);
|
||||
},
|
||||
saveSelectedDisplayFilter() {
|
||||
this.updateUISettings({
|
||||
inbox_filter_by: {
|
||||
...this.activeDisplayFilter,
|
||||
sort_by: this.activeSort || wootConstants.INBOX_SORT_BY.NEWEST,
|
||||
},
|
||||
});
|
||||
},
|
||||
setSavedFilter() {
|
||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { status, type, sort_by: sortBy } = filterBy;
|
||||
this.activeSort = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
this.displayOptions.forEach(option => {
|
||||
option.selected =
|
||||
option.type === wootConstants.INBOX_FILTER_TYPE.STATUS
|
||||
? option.key === status
|
||||
: option.key === type;
|
||||
this.activeDisplayFilter[option.type] = option.selected
|
||||
? option.key
|
||||
: '';
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex gap-2 py-2 pl-4 h-14 pr-2 justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
|
||||
class="flex gap-2 py-2 ltr:pl-4 rtl:pl-2 h-14 ltr:pr-2 rtl:pr-4 rtl:border-r justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<pagination-button
|
||||
v-if="totalLength > 1"
|
||||
:total-length="totalLength"
|
||||
:current-index="currentIndex"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
<div v-else />
|
||||
<div class="flex items-center gap-2">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="snooze"
|
||||
@click="onSnooze"
|
||||
@click="openSnoozeNotificationModal"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.SNOOZE') }}
|
||||
</woot-button>
|
||||
@@ -23,20 +25,39 @@
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
@click="onDelete"
|
||||
@click="deleteNotification"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="scheduleCustomSnooze"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import PaginationButton from './PaginationButton.vue';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PaginationButton,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
totalLength: {
|
||||
type: Number,
|
||||
@@ -46,10 +67,73 @@ export default {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeNotification: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCustomSnoozeModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
meta: 'notifications/getMeta',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
},
|
||||
methods: {
|
||||
onSnooze() {},
|
||||
onDelete() {},
|
||||
openSnoozeNotificationModal() {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open({ parent: 'snooze_notification' });
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
snoozeNotification(snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('notifications/snooze', {
|
||||
id: this.activeNotification?.id,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.SNOOZE'));
|
||||
});
|
||||
},
|
||||
onCmdSnoozeNotification(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
const snoozedUntil = findSnoozeTime(snoozeType) || null;
|
||||
this.snoozeNotification(snoozedUntil);
|
||||
}
|
||||
},
|
||||
scheduleCustomSnooze(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
const snoozedUntil = getUnixTime(customSnoozeTime) || null;
|
||||
this.snoozeNotification(snoozedUntil);
|
||||
}
|
||||
},
|
||||
deleteNotification() {
|
||||
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||
this.$store
|
||||
.dispatch('notifications/delete', {
|
||||
notification: this.activeNotification,
|
||||
unread_count: this.meta.unreadCount,
|
||||
count: this.meta.count,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.DELETE'));
|
||||
});
|
||||
this.$router.push({ name: 'inbox_view' });
|
||||
},
|
||||
onClickNext() {
|
||||
this.$emit('next');
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full pl-4 pr-2 py-2 h-14 justify-between items-center border-b border-slate-50 dark:border-slate-800/50"
|
||||
class="flex w-full ltr:pl-4 rtl:pl-2 rtl:pr-4 ltr:pr-2 py-2 h-14 justify-between items-center border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<h1 class="font-medium text-slate-900 dark:text-slate-25 text-xl">
|
||||
@@ -27,6 +27,7 @@
|
||||
v-if="showInboxDisplayMenu"
|
||||
v-on-clickaway="openInboxDisplayMenu"
|
||||
class="absolute top-8"
|
||||
@filter="onFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,6 +50,7 @@
|
||||
v-if="showInboxOptionMenu"
|
||||
v-on-clickaway="openInboxOptionsMenu"
|
||||
class="absolute top-9"
|
||||
@option-click="onInboxOptionMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,13 +59,16 @@
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import InboxOptionMenu from './InboxOptionMenu.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import InboxDisplayMenu from './InboxDisplayMenu.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxOptionMenu,
|
||||
InboxDisplayMenu,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
showInboxDisplayMenu: false,
|
||||
@@ -71,12 +76,46 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
markAllRead() {
|
||||
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
|
||||
this.$store.dispatch('notifications/readAll').then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.MARK_ALL_READ'));
|
||||
});
|
||||
},
|
||||
deleteAll() {
|
||||
this.$store.dispatch('notifications/deleteAll').then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.DELETE_ALL'));
|
||||
});
|
||||
},
|
||||
deleteAllRead() {
|
||||
this.$store.dispatch('notifications/deleteAllRead').then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.DELETE_ALL_READ'));
|
||||
});
|
||||
},
|
||||
openInboxDisplayMenu() {
|
||||
this.showInboxDisplayMenu = !this.showInboxDisplayMenu;
|
||||
},
|
||||
openInboxOptionsMenu() {
|
||||
this.showInboxOptionMenu = !this.showInboxOptionMenu;
|
||||
},
|
||||
onInboxOptionMenuClick(key) {
|
||||
this.showInboxOptionMenu = false;
|
||||
if (key === 'mark_all_read') {
|
||||
this.markAllRead();
|
||||
}
|
||||
if (key === 'delete_all') {
|
||||
this.deleteAll();
|
||||
}
|
||||
if (key === 'delete_all_read') {
|
||||
this.deleteAllRead();
|
||||
}
|
||||
},
|
||||
onFilterChange(option) {
|
||||
this.$emit('filter', option);
|
||||
this.showInboxDisplayMenu = false;
|
||||
if (this.$route.name === 'inbox_view') return;
|
||||
this.$router.push({ name: 'inbox_view' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-1 bg-white z-50 dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-500 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
class="flex flex-col gap-1 bg-white z-50 dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onMenuItemClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<menu-item
|
||||
v-for="item in commonMenuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onMenuItemClick(item.key)"
|
||||
@click="onClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,24 +22,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
menuItems: [
|
||||
{
|
||||
key: 'mark_as_read',
|
||||
label: this.$t('INBOX.MENU_ITEM.MARK_AS_READ'),
|
||||
},
|
||||
{
|
||||
key: 'mark_as_unread',
|
||||
label: this.$t('INBOX.MENU_ITEM.MARK_AS_UNREAD'),
|
||||
},
|
||||
{
|
||||
key: 'snooze',
|
||||
label: this.$t('INBOX.MENU_ITEM.SNOOZE'),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: this.$t('INBOX.MENU_ITEM.DELETE'),
|
||||
},
|
||||
],
|
||||
commonMenuItems: [
|
||||
{
|
||||
key: 'mark_all_read',
|
||||
label: this.$t('INBOX.MENU_ITEM.MARK_ALL_READ'),
|
||||
@@ -64,7 +38,7 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onMenuItemClick(key) {
|
||||
onClick(key) {
|
||||
this.$emit('option-click', key);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildConversationList,
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
isOnFoldersView,
|
||||
} from './helpers/actionHelpers';
|
||||
import messageReadActions from './actions/messageReadActions';
|
||||
import messageTranslateActions from './actions/messageTranslateActions';
|
||||
@@ -321,12 +322,12 @@ const actions = {
|
||||
inbox_id: inboxId,
|
||||
meta: { sender },
|
||||
} = conversation;
|
||||
|
||||
const hasAppliedFilters = !!appliedFilters.length;
|
||||
const isMatchingInboxFilter =
|
||||
!currentInbox || Number(currentInbox) === inboxId;
|
||||
if (
|
||||
!hasAppliedFilters &&
|
||||
!isOnFoldersView(rootState) &&
|
||||
!isOnMentionsView(rootState) &&
|
||||
!isOnUnattendedView(rootState) &&
|
||||
isMatchingInboxFilter
|
||||
|
||||
@@ -30,6 +30,14 @@ export const isOnUnattendedView = ({ route: { name: routeName } }) => {
|
||||
return UNATTENDED_ROUTES.includes(routeName);
|
||||
};
|
||||
|
||||
export const isOnFoldersView = ({ route: { name: routeName } }) => {
|
||||
const FOLDER_ROUTES = [
|
||||
'folder_conversations',
|
||||
'conversations_through_folders',
|
||||
];
|
||||
return FOLDER_ROUTES.includes(routeName);
|
||||
};
|
||||
|
||||
export const buildConversationList = (
|
||||
context,
|
||||
requestPayload,
|
||||
|
||||
+15
-1
@@ -1,4 +1,4 @@
|
||||
import { isOnMentionsView } from '../actionHelpers';
|
||||
import { isOnMentionsView, isOnFoldersView } from '../actionHelpers';
|
||||
|
||||
describe('#isOnMentionsView', () => {
|
||||
it('return valid responses when passing the state', () => {
|
||||
@@ -10,3 +10,17 @@ describe('#isOnMentionsView', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isOnFoldersView', () => {
|
||||
it('return valid responses when passing the state', () => {
|
||||
expect(isOnFoldersView({ route: { name: 'folder_conversations' } })).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
isOnFoldersView({ route: { name: 'conversations_through_folders' } })
|
||||
).toBe(true);
|
||||
expect(isOnFoldersView({ route: { name: 'conversation_messages' } })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export const actions = {
|
||||
data: {
|
||||
data: { payload, meta },
|
||||
},
|
||||
} = await NotificationsAPI.get(page);
|
||||
} = await NotificationsAPI.get({ page });
|
||||
commit(types.CLEAR_NOTIFICATIONS);
|
||||
commit(types.SET_NOTIFICATIONS, payload);
|
||||
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||
@@ -18,14 +18,19 @@ export const actions = {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
index: async ({ commit }, { page = 1 } = {}) => {
|
||||
index: async ({ commit }, { page = 1, status, type, sortOrder } = {}) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
data: { payload, meta },
|
||||
},
|
||||
} = await NotificationsAPI.get(page);
|
||||
} = await NotificationsAPI.get({
|
||||
page,
|
||||
status,
|
||||
type,
|
||||
sortOrder,
|
||||
});
|
||||
commit(types.SET_NOTIFICATIONS, payload);
|
||||
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
@@ -54,7 +59,7 @@ export const actions = {
|
||||
try {
|
||||
await NotificationsAPI.read(primaryActorType, primaryActorId);
|
||||
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, unreadCount - 1);
|
||||
commit(types.UPDATE_NOTIFICATION, { id, read_at: new Date() });
|
||||
commit(types.READ_NOTIFICATION, { id, read_at: new Date() });
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
@@ -64,7 +69,7 @@ export const actions = {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await NotificationsAPI.unRead(id);
|
||||
commit(types.UPDATE_NOTIFICATION, { id, read_at: null });
|
||||
commit(types.READ_NOTIFICATION, { id, read_at: null });
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
@@ -95,6 +100,56 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteAllRead: async ({ commit }) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await NotificationsAPI.deleteAll({
|
||||
type: 'read',
|
||||
});
|
||||
commit(types.DELETE_READ_NOTIFICATIONS);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
deleteAll: async ({ commit }) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await NotificationsAPI.deleteAll({
|
||||
type: 'all',
|
||||
});
|
||||
commit(types.DELETE_ALL_NOTIFICATIONS);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
snooze: async ({ commit }, { id, snoozedUntil }) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await NotificationsAPI.snooze({
|
||||
id,
|
||||
snoozedUntil,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { snoozed_until = null },
|
||||
} = response;
|
||||
commit(types.SNOOZE_NOTIFICATION, {
|
||||
id,
|
||||
snoozed_until,
|
||||
});
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateNotification: async ({ commit }, data) => {
|
||||
commit(types.UPDATE_NOTIFICATION, data);
|
||||
},
|
||||
|
||||
addNotification({ commit }, data) {
|
||||
commit(types.ADD_NOTIFICATION, data);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { sortComparator } from './helpers';
|
||||
|
||||
export const getters = {
|
||||
getNotifications($state) {
|
||||
return Object.values($state.records).sort((n1, n2) => n2.id - n1.id);
|
||||
},
|
||||
getFilteredNotifications: $state => filters => {
|
||||
const sortOrder = filters.sortOrder === 'desc' ? 'newest' : 'oldest';
|
||||
const sortedNotifications = Object.values($state.records).sort((n1, n2) =>
|
||||
sortComparator(n1, n2, sortOrder)
|
||||
);
|
||||
return sortedNotifications;
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const INBOX_SORT_OPTIONS = {
|
||||
newest: 'desc',
|
||||
oldest: 'asc',
|
||||
};
|
||||
|
||||
const sortConfig = {
|
||||
newest: (a, b) => b.created_at - a.created_at,
|
||||
oldest: (a, b) => a.created_at - b.created_at,
|
||||
};
|
||||
|
||||
export const sortComparator = (a, b, sortOrder) => {
|
||||
const sortDirection = INBOX_SORT_OPTIONS[sortOrder];
|
||||
if (sortOrder === 'newest' || sortOrder === 'oldest') {
|
||||
return sortConfig[sortOrder](a, b, sortDirection);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const mutations = {
|
||||
});
|
||||
});
|
||||
},
|
||||
[types.UPDATE_NOTIFICATION]: ($state, { id, read_at }) => {
|
||||
[types.READ_NOTIFICATION]: ($state, { id, read_at }) => {
|
||||
Vue.set($state.records[id], 'read_at', read_at);
|
||||
},
|
||||
[types.UPDATE_ALL_NOTIFICATIONS]: $state => {
|
||||
@@ -52,6 +52,15 @@ export const mutations = {
|
||||
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||
Vue.set($state.meta, 'count', count);
|
||||
},
|
||||
[types.UPDATE_NOTIFICATION]($state, data) {
|
||||
const { notification, unread_count: unreadCount, count } = data;
|
||||
Vue.set($state.records, notification.id, {
|
||||
...($state.records[notification.id] || {}),
|
||||
...notification,
|
||||
});
|
||||
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||
Vue.set($state.meta, 'count', count);
|
||||
},
|
||||
[types.DELETE_NOTIFICATION]($state, data) {
|
||||
const { notification, unread_count: unreadCount, count } = data;
|
||||
Vue.delete($state.records, notification.id);
|
||||
@@ -61,4 +70,19 @@ export const mutations = {
|
||||
[types.SET_ALL_NOTIFICATIONS_LOADED]: $state => {
|
||||
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', true);
|
||||
},
|
||||
|
||||
[types.DELETE_READ_NOTIFICATIONS]: $state => {
|
||||
Object.values($state.records).forEach(item => {
|
||||
if (item.read_at) {
|
||||
Vue.delete($state.records, item.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
[types.DELETE_ALL_NOTIFICATIONS]: $state => {
|
||||
Vue.set($state, 'records', {});
|
||||
},
|
||||
|
||||
[types.SNOOZE_NOTIFICATION]: ($state, { id, snoozed_until }) => {
|
||||
Vue.set($state.records[id], 'snoozed_until', snoozed_until);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -181,6 +181,26 @@ describe('#actions', () => {
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('doesnot send mutation if the view is conversation folders', () => {
|
||||
const conversation = {
|
||||
id: 1,
|
||||
messages: [],
|
||||
meta: { sender: { id: 1, name: 'john-doe' } },
|
||||
inbox_id: 1,
|
||||
};
|
||||
actions.addConversation(
|
||||
{
|
||||
commit,
|
||||
rootState: { route: { name: 'folder_conversations' } },
|
||||
dispatch,
|
||||
state: { currentInbox: 1, appliedFilters: [{ id: 'random-filter' }] },
|
||||
},
|
||||
conversation
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('sends correct mutations', () => {
|
||||
const conversation = {
|
||||
id: 1,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../notifications/actions';
|
||||
import types from '../../../mutation-types';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
@@ -101,7 +100,7 @@ describe('#actions', () => {
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 1],
|
||||
[types.UPDATE_NOTIFICATION, { id: 1, read_at: expect.any(Date) }],
|
||||
[types.READ_NOTIFICATION, { id: 1, read_at: expect.any(Date) }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
@@ -125,7 +124,7 @@ describe('#actions', () => {
|
||||
await actions.unread({ commit }, { id: 1 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_NOTIFICATIONS_UI_FLAG', { isUpdating: true }],
|
||||
['UPDATE_NOTIFICATION', { id: 1, read_at: null }],
|
||||
['READ_NOTIFICATION', { id: 1, read_at: null }],
|
||||
['SET_NOTIFICATIONS_UI_FLAG', { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
@@ -219,4 +218,69 @@ describe('#actions', () => {
|
||||
expect(commit.mock.calls).toEqual([[types.CLEAR_NOTIFICATIONS]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAllRead', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({});
|
||||
await actions.deleteAllRead({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_READ_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.deleteAllRead({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_READ_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({});
|
||||
await actions.deleteAll({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_ALL_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.deleteAll({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_ALL_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snooze', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: { snoozed_until: '20 Jan, 5.04pm' },
|
||||
});
|
||||
await actions.snooze({ commit }, { id: 1, snoozedUntil: 1703057715 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SNOOZE_NOTIFICATION, { id: 1, snoozed_until: '20 Jan, 5.04pm' }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.snooze({ commit }, { id: 1, snoozedUntil: 1703057715 });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,34 @@ describe('#getters', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('getFilteredNotifications', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1, read_at: '2024-02-07T11:42:39.988Z', snoozed_until: null },
|
||||
2: { id: 2, read_at: null, snoozed_until: null },
|
||||
3: {
|
||||
id: 3,
|
||||
read_at: '2024-02-07T11:42:39.988Z',
|
||||
snoozed_until: '2024-02-07T11:42:39.988Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
const filters = {
|
||||
type: 'read',
|
||||
status: 'snoozed',
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
expect(getters.getFilteredNotifications(state)(filters)).toEqual([
|
||||
{ id: 1, read_at: '2024-02-07T11:42:39.988Z', snoozed_until: null },
|
||||
{ id: 2, read_at: null, snoozed_until: null },
|
||||
{
|
||||
id: 3,
|
||||
read_at: '2024-02-07T11:42:39.988Z',
|
||||
snoozed_until: '2024-02-07T11:42:39.988Z',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { sortComparator } from '../../notifications/helpers';
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
id: 1,
|
||||
read_at: '2024-02-07T11:42:39.988Z',
|
||||
snoozed_until: null,
|
||||
created_at: 1707328400,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
read_at: null,
|
||||
snoozed_until: null,
|
||||
created_at: 1707233688,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
read_at: '2024-01-07T11:42:39.988Z',
|
||||
snoozed_until: null,
|
||||
created_at: 1707233672,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
read_at: null,
|
||||
snoozed_until: '2024-02-08T03:30:00.000Z',
|
||||
created_at: 1707233667,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
read_at: '2024-02-07T10:42:39.988Z',
|
||||
snoozed_until: '2024-02-08T03:30:00.000Z',
|
||||
created_at: 1707233662,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
read_at: null,
|
||||
snoozed_until: '2024-02-08T03:30:00.000Z',
|
||||
created_at: 1707233561,
|
||||
},
|
||||
];
|
||||
|
||||
describe('#sortComparator', () => {
|
||||
it('returns the notifications sorted by newest', () => {
|
||||
const sortOrder = 'newest';
|
||||
const sortedNotifications = [...notifications].sort((a, b) =>
|
||||
sortComparator(a, b, sortOrder)
|
||||
);
|
||||
const expectedOrder = [
|
||||
notifications[0],
|
||||
notifications[1],
|
||||
notifications[2],
|
||||
notifications[3],
|
||||
notifications[4],
|
||||
notifications[5],
|
||||
].sort((a, b) => b.created_at - a.created_at);
|
||||
expect(sortedNotifications).toEqual(expectedOrder);
|
||||
});
|
||||
|
||||
it('returns the notifications sorted by oldest', () => {
|
||||
const sortOrder = 'oldest';
|
||||
const sortedNotifications = [...notifications].sort((a, b) =>
|
||||
sortComparator(a, b, sortOrder)
|
||||
);
|
||||
const expectedOrder = [
|
||||
notifications[0],
|
||||
notifications[1],
|
||||
notifications[2],
|
||||
notifications[3],
|
||||
notifications[4],
|
||||
notifications[5],
|
||||
].sort((a, b) => a.created_at - b.created_at);
|
||||
expect(sortedNotifications).toEqual(expectedOrder);
|
||||
});
|
||||
});
|
||||
@@ -75,7 +75,7 @@ describe('#mutations', () => {
|
||||
1: { id: 1, primary_actor_id: 1 },
|
||||
},
|
||||
};
|
||||
mutations[types.UPDATE_NOTIFICATION](state, {
|
||||
mutations[types.READ_NOTIFICATION](state, {
|
||||
id: 1,
|
||||
read_at: true,
|
||||
});
|
||||
@@ -155,4 +155,32 @@ describe('#mutations', () => {
|
||||
expect(state.uiFlags).toEqual({ isAllNotificationsLoaded: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_READ_NOTIFICATIONS', () => {
|
||||
it('delete read notifications', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
},
|
||||
};
|
||||
mutations[types.DELETE_READ_NOTIFICATIONS](state);
|
||||
expect(state.records).toEqual({
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_ALL_NOTIFICATIONS', () => {
|
||||
it('delete all notifications', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
},
|
||||
};
|
||||
mutations[types.DELETE_ALL_NOTIFICATIONS](state);
|
||||
expect(state.records).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,6 +132,7 @@ export default {
|
||||
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
|
||||
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
|
||||
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
|
||||
READ_NOTIFICATION: 'READ_NOTIFICATION',
|
||||
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
|
||||
DELETE_NOTIFICATION: 'DELETE_NOTIFICATION',
|
||||
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
|
||||
@@ -141,6 +142,9 @@ export default {
|
||||
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||
SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED',
|
||||
DELETE_READ_NOTIFICATIONS: 'DELETE_READ_NOTIFICATIONS',
|
||||
DELETE_ALL_NOTIFICATIONS: 'DELETE_ALL_NOTIFICATIONS',
|
||||
SNOOZE_NOTIFICATION: 'SNOOZE_NOTIFICATION',
|
||||
|
||||
// Contact Conversation
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
v-if="label"
|
||||
:for="name"
|
||||
class="flex justify-between text-sm font-medium leading-6 text-slate-900 dark:text-white"
|
||||
:class="{ 'text-red-500': hasError }"
|
||||
>
|
||||
{{ label }}
|
||||
<with-label
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<template #rightOfLabel>
|
||||
<slot />
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:class="{
|
||||
'focus:ring-red-600 ring-red-600': hasError,
|
||||
'dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200':
|
||||
!hasError,
|
||||
}"
|
||||
class="block w-full rounded-md border-0 px-3 py-3 appearance-none shadow-sm ring-1 ring-inset text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 outline-none dark:bg-slate-700"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
<span
|
||||
v-if="errorMessage && hasError"
|
||||
class="text-xs leading-2 text-red-400"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:tabindex="tabindex"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
|
||||
hasError,
|
||||
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'px-3 py-3': spacing === 'base',
|
||||
'px-3 py-2 mb-0': spacing === 'compact',
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full border-none shadow-sm appearance-none rounded-xl outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
@@ -52,6 +53,10 @@ export default {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -64,6 +69,10 @@ export default {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -76,6 +85,11 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
spacing: {
|
||||
type: String,
|
||||
default: 'base',
|
||||
validator: value => ['base', 'compact'].includes(value),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<woot-button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:variant="value === option.value ? '' : 'hollow'"
|
||||
:color-scheme="value === option.value ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
@click="$emit('input', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</with-label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<select
|
||||
:id="id"
|
||||
:selected="value"
|
||||
:name="name"
|
||||
:class="{
|
||||
'text-slate-400': !value,
|
||||
'text-slate-900 dark:text-slate-100': value,
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="mb-0 block w-full px-3 py-2 pr-6 border-0 rounded-xl shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
|
||||
@input="onInput"
|
||||
>
|
||||
<option value="" disabled selected class="hidden">
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<slot>
|
||||
<option v-for="opt in options" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</slot>
|
||||
</select>
|
||||
</with-label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.select-caret {
|
||||
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-origin: content-box;
|
||||
background-position: right -1rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9px 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<textarea
|
||||
:id="name"
|
||||
:name="name"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600': hasError,
|
||||
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'resize-none': !allowResize,
|
||||
}"
|
||||
class="block w-full p-3 border-none rounded-xl shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 bg-white dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
allowResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dataTestid: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="name"
|
||||
class="flex justify-between text-sm font-medium leading-6 text-slate-900 dark:text-white"
|
||||
:class="{ 'text-red-500': hasError }"
|
||||
>
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
<slot name="rightOfLabel" />
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center relative w-full">
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
size="16"
|
||||
:icon="icon"
|
||||
class="absolute left-2 transform text-slate-400 dark:text-slate-600 w-5 h-5"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
<span
|
||||
v-if="errorMessage && hasError"
|
||||
class="text-xs text-red-400 leading-2"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -46,6 +46,7 @@
|
||||
name="email_address"
|
||||
type="text"
|
||||
data-testid="email_input"
|
||||
:tabindex="1"
|
||||
required
|
||||
:label="$t('LOGIN.EMAIL.LABEL')"
|
||||
:placeholder="$t('LOGIN.EMAIL.PLACEHOLDER')"
|
||||
@@ -58,19 +59,25 @@
|
||||
name="password"
|
||||
data-testid="password_input"
|
||||
required
|
||||
:tabindex="2"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('LOGIN.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
@input="$v.credentials.password.$touch"
|
||||
>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link to="auth/reset/password" class="text-link">
|
||||
<router-link
|
||||
to="auth/reset/password"
|
||||
class="text-link text-sm"
|
||||
tabindex="4"
|
||||
>
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</form-input>
|
||||
<submit-button
|
||||
:disabled="loginApi.showLoading"
|
||||
:tabindex="3"
|
||||
:button-text="$t('LOGIN.SUBMIT')"
|
||||
:loading="loginApi.showLoading"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 text-lg font-bold mb-6 relative before:absolute before:h-10 before:w-[1px] before:bg-slate-200 before:-bottom-8 before:left-[24px] hide-before-of-last"
|
||||
:class="{
|
||||
'text-woot-500 ': isActive,
|
||||
'text-slate-400': !isActive || isComplete,
|
||||
'before:bg-woot-500': !isActive && isComplete,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="grid w-8 h-8 border border-solid rounded-full place-content-center"
|
||||
:class="{
|
||||
'border-woot-500': !isActive || isComplete,
|
||||
'border-slate-200 dark:border-slate-600': !isActive && !isComplete,
|
||||
'text-woot-500': isComplete,
|
||||
}"
|
||||
>
|
||||
<fluent-icon v-if="isComplete" size="20" icon="checkmark" />
|
||||
<fluent-icon v-else size="20" :icon="icon" />
|
||||
</div>
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OnboardingStep',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isComplete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.hide-before-of-last:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
||||
return unless should_fetch_email?(channel)
|
||||
|
||||
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
|
||||
|
||||
with_lock(key, 5.minutes) do
|
||||
process_email_for_channel(channel)
|
||||
end
|
||||
@@ -28,128 +29,16 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
||||
end
|
||||
|
||||
def process_email_for_channel(channel)
|
||||
if channel.microsoft?
|
||||
fetch_mail_for_ms_provider(channel)
|
||||
else
|
||||
fetch_mail_for_channel(channel)
|
||||
end
|
||||
# clearing old failures like timeouts since the mail is now successfully processed
|
||||
channel.reauthorized!
|
||||
end
|
||||
|
||||
def fetch_mail_for_channel(channel)
|
||||
imap_client = build_imap_client(channel, channel.imap_password, 'PLAIN')
|
||||
|
||||
message_ids_with_seq = fetch_message_ids_with_sequence(imap_client, channel)
|
||||
message_ids_with_seq.each do |message_id_with_seq|
|
||||
process_message_id(channel, imap_client, message_id_with_seq)
|
||||
end
|
||||
end
|
||||
|
||||
def process_message_id(channel, imap_client, message_id_with_seq)
|
||||
seq_no, message_id = message_id_with_seq
|
||||
|
||||
return if email_already_present?(channel, message_id)
|
||||
|
||||
if message_id.blank?
|
||||
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Empty message id for #{channel.email} with seq no. <#{seq_no}>."
|
||||
return
|
||||
end
|
||||
|
||||
# Fetch the original mail content using the sequence no
|
||||
mail_str = imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']
|
||||
|
||||
if mail_str.blank?
|
||||
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetch failed for #{channel.email} with message-id <#{message_id}>."
|
||||
return
|
||||
end
|
||||
|
||||
inbound_mail = build_mail_from_string(mail_str)
|
||||
mail_info_logger(channel, inbound_mail, seq_no)
|
||||
process_mail(inbound_mail, channel)
|
||||
end
|
||||
|
||||
# Sends a FETCH command to retrieve data associated with a message in the mailbox.
|
||||
# You can send batches of message sequence number in `.fetch` method.
|
||||
def fetch_message_ids_with_sequence(imap_client, channel)
|
||||
seq_nums = fetch_available_mail_sequence_numbers(imap_client)
|
||||
|
||||
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{channel.email}, found #{seq_nums.length}."
|
||||
|
||||
message_ids_with_seq = []
|
||||
seq_nums.each_slice(10).each do |batch|
|
||||
# Fetch only message-id only without mail body or contents.
|
||||
batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER]')
|
||||
|
||||
# .fetch returns an array of Net::IMAP::FetchData or nil
|
||||
# (instead of an empty array) if there is no matching message.
|
||||
# Check
|
||||
if batch_message_ids.blank?
|
||||
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}."
|
||||
next
|
||||
end
|
||||
|
||||
batch_message_ids.each do |data|
|
||||
message_id = build_mail_from_string(data.attr['BODY[HEADER]']).message_id
|
||||
message_ids_with_seq.push([data.seqno, message_id])
|
||||
end
|
||||
end
|
||||
|
||||
message_ids_with_seq
|
||||
end
|
||||
|
||||
# Sends a SEARCH command to search the mailbox for messages that were
|
||||
# created between yesterday and today and returns message sequence numbers.
|
||||
# Return <message set>
|
||||
def fetch_available_mail_sequence_numbers(imap_client)
|
||||
imap_client.search(['SINCE', yesterday])
|
||||
end
|
||||
|
||||
def fetch_mail_for_ms_provider(channel)
|
||||
return if channel.provider_config['access_token'].blank?
|
||||
|
||||
access_token = valid_access_token channel
|
||||
|
||||
return unless access_token
|
||||
|
||||
imap_client = build_imap_client(channel, access_token, 'XOAUTH2')
|
||||
process_mails(imap_client, channel)
|
||||
end
|
||||
|
||||
def process_mails(imap_client, channel)
|
||||
fetch_available_mail_sequence_numbers(imap_client).each do |seq_no|
|
||||
inbound_mail = Mail.read_from_string imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']
|
||||
|
||||
mail_info_logger(channel, inbound_mail, seq_no)
|
||||
|
||||
next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
|
||||
|
||||
inbound_emails = if channel.microsoft?
|
||||
Imap::MicrosoftFetchEmailService.new(channel: channel).perform
|
||||
else
|
||||
Imap::FetchEmailService.new(channel: channel).perform
|
||||
end
|
||||
inbound_emails.map do |inbound_mail|
|
||||
process_mail(inbound_mail, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def mail_info_logger(channel, inbound_mail, uid)
|
||||
return if Rails.env.test?
|
||||
|
||||
Rails.logger.info("
|
||||
#{channel.provider} Email id: #{inbound_mail.from} - message_source_id: #{inbound_mail.message_id} - sequence id: #{uid}")
|
||||
end
|
||||
|
||||
def build_imap_client(channel, access_token, auth_method)
|
||||
imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true)
|
||||
imap.authenticate(auth_method, channel.imap_login, access_token)
|
||||
imap.select('INBOX')
|
||||
imap
|
||||
end
|
||||
|
||||
def email_already_present?(channel, message_id)
|
||||
channel.inbox.messages.find_by(source_id: message_id).present?
|
||||
end
|
||||
|
||||
def build_mail_from_string(raw_email_content)
|
||||
Mail.read_from_string(raw_email_content)
|
||||
end
|
||||
|
||||
def process_mail(inbound_mail, channel)
|
||||
Imap::ImapMailbox.new.process(inbound_mail, channel)
|
||||
rescue StandardError => e
|
||||
@@ -157,13 +46,4 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
||||
Rails.logger.error("
|
||||
#{channel.provider} Email dropped: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}")
|
||||
end
|
||||
|
||||
# Making sure the access token is valid for microsoft provider
|
||||
def valid_access_token(channel)
|
||||
Microsoft::RefreshOauthTokenService.new(channel: channel).access_token
|
||||
end
|
||||
|
||||
def yesterday
|
||||
(Time.zone.today - 1).strftime('%d-%b-%Y')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class Notification::DeleteNotificationJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(user, type: :all)
|
||||
ActiveRecord::Base.transaction do
|
||||
if type == :all
|
||||
# Delete all notifications
|
||||
user.notifications.destroy_all
|
||||
elsif type == :read
|
||||
# Delete only read notifications
|
||||
user.notifications.where.not(read_at: nil).destroy_all
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class Notification::ReopenSnoozedNotificationsJob < ApplicationJob
|
||||
def perform
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Notification.where(snoozed_until: 3.days.ago..Time.current)
|
||||
.update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current)
|
||||
.update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current, read_at: nil)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,3 +24,5 @@ class TriggerScheduledItemsJob < ApplicationJob
|
||||
Notification::RemoveOldNotificationJob.perform_later
|
||||
end
|
||||
end
|
||||
|
||||
TriggerScheduledItemsJob.prepend_mod_with('TriggerScheduledItemsJob')
|
||||
|
||||
@@ -7,6 +7,12 @@ class ActionCableListener < BaseListener
|
||||
broadcast(account, tokens, NOTIFICATION_CREATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
||||
end
|
||||
|
||||
def notification_updated(event)
|
||||
notification, account, unread_count, count = extract_notification_and_account(event)
|
||||
tokens = [event.data[:notification].user.pubsub_token]
|
||||
broadcast(account, tokens, NOTIFICATION_UPDATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
||||
end
|
||||
|
||||
def notification_deleted(event)
|
||||
return if event.data[:notification].user.blank?
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
class NotificationListener < BaseListener
|
||||
def conversation_bot_handoff(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
return if conversation.pending?
|
||||
|
||||
conversation.inbox.members.each do |agent|
|
||||
NotificationBuilder.new(
|
||||
notification_type: 'conversation_creation',
|
||||
user: agent,
|
||||
account: account,
|
||||
primary_actor: conversation
|
||||
).perform
|
||||
end
|
||||
end
|
||||
|
||||
def conversation_created(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
return if conversation.pending?
|
||||
|
||||
@@ -2,6 +2,7 @@ module MailboxHelper
|
||||
private
|
||||
|
||||
def create_message
|
||||
Rails.logger.info "[MailboxHelper] Creating message #{processed_mail.message_id}"
|
||||
return if @conversation.messages.find_by(source_id: processed_mail.message_id).present?
|
||||
|
||||
@message = @conversation.messages.create!(
|
||||
@@ -36,6 +37,7 @@ module MailboxHelper
|
||||
end
|
||||
|
||||
def process_regular_attachments(attachments)
|
||||
Rails.logger.info "[MailboxHelper] Processing regular attachments for message with ID: #{processed_mail.message_id}"
|
||||
attachments.each do |mail_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @conversation.account_id,
|
||||
@@ -46,6 +48,8 @@ module MailboxHelper
|
||||
end
|
||||
|
||||
def process_inline_attachments(attachments)
|
||||
Rails.logger.info "[MailboxHelper] Processing inline attachments for message with ID: #{processed_mail.message_id}"
|
||||
|
||||
# create an instance variable here, the `embed_inline_image_source`
|
||||
# updates them directly. And then the value is eventaully used to update the message content
|
||||
@html_content = processed_mail.serialized_data[:html_content][:full]
|
||||
@@ -98,7 +102,9 @@ module MailboxHelper
|
||||
}
|
||||
}
|
||||
).perform
|
||||
|
||||
@contact = @contact_inbox.contact
|
||||
Rails.logger.info "[MailboxHelper] Contact created with ID: #{@contact.id} for inbox with ID: #{@inbox.id}"
|
||||
end
|
||||
|
||||
def notification_email_from_chatwoot?
|
||||
|
||||
@@ -100,7 +100,7 @@ class Account < ApplicationRecord
|
||||
.where(context: 'labels',
|
||||
taggable_type: 'Conversation',
|
||||
taggable_id: conversation_ids)
|
||||
.map { |_| _.tag.name }
|
||||
.map { |tagging| tagging.tag.name }
|
||||
end
|
||||
|
||||
def webhook_data
|
||||
@@ -111,11 +111,12 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def inbound_email_domain
|
||||
domain || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN', false)
|
||||
domain.presence || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN',
|
||||
false)
|
||||
end
|
||||
|
||||
def support_email
|
||||
super || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
|
||||
super.presence || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
|
||||
end
|
||||
|
||||
def usage_limits
|
||||
|
||||
@@ -43,7 +43,7 @@ class Article < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :category, optional: true
|
||||
belongs_to :portal
|
||||
belongs_to :author, class_name: 'User'
|
||||
belongs_to :author, class_name: 'User', inverse_of: :articles
|
||||
|
||||
before_validation :ensure_account_id
|
||||
before_validation :ensure_article_slug
|
||||
|
||||
@@ -38,7 +38,8 @@ class Attachment < ApplicationRecord
|
||||
has_one_attached :file
|
||||
validate :acceptable_file
|
||||
validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
||||
enum file_type: [:image, :audio, :video, :file, :location, :fallback, :share, :story_mention, :contact]
|
||||
enum file_type: { :image => 0, :audio => 1, :video => 2, :file => 3, :location => 4, :fallback => 5, :share => 6, :story_mention => 7,
|
||||
:contact => 8 }
|
||||
|
||||
def push_event_data
|
||||
return unless file_type
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
#
|
||||
|
||||
class CannedResponse < ApplicationRecord
|
||||
validates_presence_of :content
|
||||
validates_presence_of :short_code
|
||||
validates_presence_of :account
|
||||
validates_uniqueness_of :short_code, scope: :account_id
|
||||
validates :content, presence: true
|
||||
validates :short_code, presence: true
|
||||
validates :account, presence: true
|
||||
validates :short_code, uniqueness: { scope: :account_id }
|
||||
|
||||
belongs_to :account
|
||||
|
||||
|
||||
@@ -46,20 +46,20 @@ class Channel::FacebookPage < ApplicationRecord
|
||||
|
||||
def subscribe
|
||||
# ref https://developers.facebook.com/docs/messenger-platform/reference/webhook-events
|
||||
response = Facebook::Messenger::Subscriptions.subscribe(
|
||||
Facebook::Messenger::Subscriptions.subscribe(
|
||||
access_token: page_access_token,
|
||||
subscribed_fields: %w[
|
||||
messages message_deliveries message_echoes message_reads standby messaging_handovers
|
||||
]
|
||||
)
|
||||
rescue => e
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||
true
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
Facebook::Messenger::Subscriptions.unsubscribe(access_token: page_access_token)
|
||||
rescue => e
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||
true
|
||||
end
|
||||
@@ -73,6 +73,7 @@ class Channel::FacebookPage < ApplicationRecord
|
||||
delete_instagram_story(message) if story_link.blank?
|
||||
story_link
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
Rails.logger.debug { "Instagram Story Expired: #{e.inspect}" }
|
||||
delete_instagram_story(message)
|
||||
end
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class Conversation < ApplicationRecord
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :inbox
|
||||
belongs_to :assignee, class_name: 'User', optional: true
|
||||
belongs_to :assignee, class_name: 'User', optional: true, inverse_of: :assigned_conversations
|
||||
belongs_to :contact
|
||||
belongs_to :contact_inbox
|
||||
belongs_to :team, optional: true
|
||||
|
||||
@@ -26,7 +26,7 @@ class CsatSurveyResponse < ApplicationRecord
|
||||
belongs_to :conversation
|
||||
belongs_to :contact
|
||||
belongs_to :message
|
||||
belongs_to :assigned_agent, class_name: 'User', optional: true
|
||||
belongs_to :assigned_agent, class_name: 'User', optional: true, inverse_of: :csat_survey_responses
|
||||
|
||||
validates :rating, presence: true, inclusion: { in: [1, 2, 3, 4, 5] }
|
||||
validates :account_id, presence: true
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ class Macro < ApplicationRecord
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :created_by,
|
||||
class_name: :User, optional: true
|
||||
class_name: :User, optional: true, inverse_of: :macros
|
||||
belongs_to :updated_by,
|
||||
class_name: :User, optional: true
|
||||
has_many_attached :files
|
||||
|
||||
@@ -118,7 +118,7 @@ class Message < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :inbox
|
||||
belongs_to :conversation, touch: true
|
||||
belongs_to :sender, polymorphic: true, required: false
|
||||
belongs_to :sender, polymorphic: true, optional: true
|
||||
|
||||
has_many :attachments, dependent: :destroy, autosave: true, before_add: :validate_attachments_limit
|
||||
has_one :csat_survey_response, dependent: :destroy_async
|
||||
@@ -139,8 +139,8 @@ class Message < ApplicationRecord
|
||||
conversation_id: conversation.display_id,
|
||||
conversation: conversation_push_event_data
|
||||
)
|
||||
data.merge!(echo_id: echo_id) if echo_id.present?
|
||||
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
||||
data[:echo_id] = echo_id if echo_id.present?
|
||||
data[:attachments] = attachments.map(&:push_event_data) if attachments.present?
|
||||
merge_sender_attributes(data)
|
||||
end
|
||||
|
||||
@@ -163,8 +163,8 @@ class Message < ApplicationRecord
|
||||
end
|
||||
|
||||
def merge_sender_attributes(data)
|
||||
data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
|
||||
data.merge!(sender: sender.push_event_data(inbox)) if sender.is_a?(AgentBot)
|
||||
data[:sender] = sender.push_event_data if sender && !sender.is_a?(AgentBot)
|
||||
data[:sender] = sender.push_event_data(inbox) if sender.is_a?(AgentBot)
|
||||
data
|
||||
end
|
||||
|
||||
@@ -184,7 +184,7 @@ class Message < ApplicationRecord
|
||||
sender: sender.try(:webhook_data),
|
||||
source_id: source_id
|
||||
}
|
||||
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
||||
data[:attachments] = attachments.map(&:push_event_data) if attachments.present?
|
||||
data
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# last_activity_at :datetime
|
||||
# meta :jsonb
|
||||
# notification_type :integer not null
|
||||
# primary_actor_type :string not null
|
||||
# read_at :datetime
|
||||
@@ -46,6 +47,7 @@ class Notification < ApplicationRecord
|
||||
before_create :set_last_activity_at
|
||||
after_create_commit :process_notification_delivery, :dispatch_create_event
|
||||
after_destroy_commit :dispatch_destroy_event
|
||||
after_update_commit :dispatch_update_event
|
||||
|
||||
PRIMARY_ACTORS = ['Conversation'].freeze
|
||||
|
||||
@@ -61,6 +63,7 @@ class Notification < ApplicationRecord
|
||||
user: user&.push_event_data,
|
||||
created_at: created_at.to_i,
|
||||
last_activity_at: last_activity_at.to_i,
|
||||
snoozed_until: snoozed_until,
|
||||
account_id: account_id
|
||||
|
||||
}
|
||||
@@ -74,7 +77,8 @@ class Notification < ApplicationRecord
|
||||
def primary_actor_data
|
||||
{
|
||||
id: primary_actor.push_event_data[:id],
|
||||
meta: primary_actor.push_event_data[:meta]
|
||||
meta: primary_actor.push_event_data[:meta],
|
||||
inbox_id: primary_actor.push_event_data[:inbox_id]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -141,6 +145,10 @@ class Notification < ApplicationRecord
|
||||
Rails.configuration.dispatcher.dispatch(NOTIFICATION_CREATED, Time.zone.now, notification: self)
|
||||
end
|
||||
|
||||
def dispatch_update_event
|
||||
Rails.configuration.dispatcher.dispatch(NOTIFICATION_UPDATED, Time.zone.now, notification: self)
|
||||
end
|
||||
|
||||
def dispatch_destroy_event
|
||||
Rails.configuration.dispatcher.dispatch(NOTIFICATION_DELETED, Time.zone.now, notification: self)
|
||||
end
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
class TelegramBot < ApplicationRecord
|
||||
belongs_to :account
|
||||
has_one :inbox, as: :channel, dependent: :destroy_async
|
||||
validates_uniqueness_of :auth_key, scope: :account_id
|
||||
validates :auth_key, uniqueness: { scope: :account_id }
|
||||
end
|
||||
|
||||
+9
-5
@@ -74,15 +74,15 @@ class User < ApplicationRecord
|
||||
has_many :accounts, through: :account_users
|
||||
accepts_nested_attributes_for :account_users
|
||||
|
||||
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify
|
||||
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify, inverse_of: :assignee
|
||||
alias_attribute :conversations, :assigned_conversations
|
||||
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify
|
||||
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
|
||||
has_many :conversation_participants, dependent: :destroy_async
|
||||
has_many :participating_conversations, through: :conversation_participants, source: :conversation
|
||||
|
||||
has_many :inbox_members, dependent: :destroy_async
|
||||
has_many :inboxes, through: :inbox_members, source: :inbox
|
||||
has_many :messages, as: :sender
|
||||
has_many :messages, as: :sender, dependent: :nullify
|
||||
has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', source: :inviter, dependent: :nullify
|
||||
|
||||
has_many :custom_filters, dependent: :destroy_async
|
||||
@@ -94,12 +94,16 @@ class User < ApplicationRecord
|
||||
has_many :notifications, dependent: :destroy_async
|
||||
has_many :team_members, dependent: :destroy_async
|
||||
has_many :teams, through: :team_members
|
||||
has_many :articles, foreign_key: 'author_id', dependent: :nullify
|
||||
has_many :articles, foreign_key: 'author_id', dependent: :nullify, inverse_of: :author
|
||||
has_many :portal_members, class_name: :PortalMember, dependent: :destroy_async
|
||||
has_many :portals, through: :portal_members, source: :portal,
|
||||
class_name: :Portal,
|
||||
dependent: :nullify
|
||||
has_many :macros, foreign_key: 'created_by_id'
|
||||
# rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
# we are handling this in `remove_macros` callback
|
||||
has_many :macros, foreign_key: 'created_by_id', inverse_of: :created_by
|
||||
# rubocop:enable Rails/HasManyOrHasOneDependent
|
||||
|
||||
before_validation :set_password_and_uid, on: :create
|
||||
after_destroy :remove_macros
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class ReportPolicy < ApplicationPolicy
|
||||
def view?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -14,4 +14,8 @@ class UserPolicy < ApplicationPolicy
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def bulk_create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user